From 65d364d8052ee993b648af3d900bc1fc480cd6c5 Mon Sep 17 00:00:00 2001 From: ebilhoo Date: Wed, 19 Apr 2017 19:54:42 +0000 Subject: [PATCH 01/89] cleanup and validation, rdpool.order optional with ROUND_ROBIN default --- .../ultradns/resource_ultradns_rdpool.go | 62 ++++++++----------- .../providers/ultradns/r/rdpool.html.markdown | 2 +- 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/builtin/providers/ultradns/resource_ultradns_rdpool.go b/builtin/providers/ultradns/resource_ultradns_rdpool.go index a45ff6939..f18870a6c 100644 --- a/builtin/providers/ultradns/resource_ultradns_rdpool.go +++ b/builtin/providers/ultradns/resource_ultradns_rdpool.go @@ -28,12 +28,6 @@ func resourceUltradnsRdpool() *schema.Resource { Required: true, ForceNew: true, }, - "order": &schema.Schema{ - Type: schema.TypeString, - Required: true, - // 0-255 char - // FIXED | RANDOM | ROUND_ROBIN - }, "rdata": &schema.Schema{ Type: schema.TypeSet, Set: schema.HashString, @@ -41,10 +35,32 @@ func resourceUltradnsRdpool() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, }, // Optional + "order": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "ROUND_ROBIN", + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value != "ROUND_ROBIN" && value != "FIXED" && value != "RANDOM" { + errors = append(errors, fmt.Errorf( + "Only 'ROUND_ROBIN', 'FIXED', and 'RANDOM' are supported values for 'order'")) + } + return + }, + }, "description": &schema.Schema{ Type: schema.TypeString, Optional: true, - // 0-255 char + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + if v != nil { + value := v.(string) + if len(value) > 255 { + errors = append(errors, fmt.Errorf( + "'description' length must be 0-255")) + } + } + return + }, }, "ttl": &schema.Schema{ Type: schema.TypeInt, @@ -111,8 +127,7 @@ func resourceUltradnsRdpoolRead(d *schema.ResourceData, meta interface{}) error r := rrsets[0] zone := d.Get("zone") - // ttl - d.Set("ttl", r.TTL) + // hostname if r.OwnerName == "" { d.Set("hostname", zone) @@ -134,11 +149,11 @@ func resourceUltradnsRdpoolRead(d *schema.ResourceData, meta interface{}) error } // Set simple values + d.Set("ttl", r.TTL) d.Set("description", p.Description) d.Set("order", p.Order) err = d.Set("rdata", makeSetFromStrings(r.RData)) - //err = d.Set("rdata", makeSetFromRdataAlone(r.RData)) if err != nil { return fmt.Errorf("rdata set failed: %#v", err) } @@ -186,13 +201,12 @@ func resourceUltradnsRdpoolDelete(d *schema.ResourceData, meta interface{}) erro func newRRSetResourceFromRdpool(d *schema.ResourceData) (rRSetResource, error) { //rDataRaw := d.Get("rdata").(*schema.Set).List() r := rRSetResource{ - // "The only valid rrtype value for SiteBacker or Traffic Controller pools is A" + // "The only valid rrtype value for RDpools is A" // per https://portal.ultradns.com/static/docs/REST-API_User_Guide.pdf RRType: "A", Zone: d.Get("zone").(string), OwnerName: d.Get("name").(string), TTL: d.Get("ttl").(int), - //RData: unzipRdataHosts(rDataRaw), } if attr, ok := d.GetOk("rdata"); ok { rdata := attr.(*schema.Set).List() @@ -213,27 +227,3 @@ func newRRSetResourceFromRdpool(d *schema.ResourceData) (rRSetResource, error) { return r, nil } - -// zip RData into []map[string]interface{} -func zipRDataAlone(rds []string) []map[string]interface{} { - result := make([]map[string]interface{}, 0, len(rds)) - for _, rd := range rds { - r := map[string]interface{}{ - // "host": rds[i], - "host": rd, - } - result = append(result, r) - } - return result -} - -// makeSetFromRdatas encodes an array of Rdata into a -// *schema.Set in the appropriate structure for the schema -func makeSetFromRdataAlone(rds []string) *schema.Set { - s := &schema.Set{F: hashRdatas} - rs := zipRDataAlone(rds) - for _, r := range rs { - s.Add(r) - } - return s -} diff --git a/website/source/docs/providers/ultradns/r/rdpool.html.markdown b/website/source/docs/providers/ultradns/r/rdpool.html.markdown index be7410fc1..80f9a3e55 100644 --- a/website/source/docs/providers/ultradns/r/rdpool.html.markdown +++ b/website/source/docs/providers/ultradns/r/rdpool.html.markdown @@ -33,8 +33,8 @@ The following arguments are supported: * `zone` - (Required) The domain to add the record to * `name` - (Required) The name of the record -* `order` - (Required) Ordering rule, one of FIXED, RANDOM or ROUND_ROBIN * `rdata` - (Required) list ip addresses. +* `order` - (Optional) Ordering rule, one of FIXED, RANDOM or ROUND_ROBIN. Default: 'ROUND_ROBIN'. * `description` - (Optional) Description of the Resource Distribution pool. Valid values are strings less than 256 characters. * `ttl` - (Optional) The TTL of the pool in seconds. Default: `3600`. From 8ef010440e381857d1256fcd352c15c5d4d6ece4 Mon Sep 17 00:00:00 2001 From: ebilhoo Date: Wed, 19 Apr 2017 19:54:42 +0000 Subject: [PATCH 02/89] cleanup and validation, rdpool.order optional with ROUND_ROBIN default --- .../ultradns/resource_ultradns_rdpool.go | 62 ++++++++----------- .../providers/ultradns/r/rdpool.html.markdown | 2 +- 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/builtin/providers/ultradns/resource_ultradns_rdpool.go b/builtin/providers/ultradns/resource_ultradns_rdpool.go index a45ff6939..f18870a6c 100644 --- a/builtin/providers/ultradns/resource_ultradns_rdpool.go +++ b/builtin/providers/ultradns/resource_ultradns_rdpool.go @@ -28,12 +28,6 @@ func resourceUltradnsRdpool() *schema.Resource { Required: true, ForceNew: true, }, - "order": &schema.Schema{ - Type: schema.TypeString, - Required: true, - // 0-255 char - // FIXED | RANDOM | ROUND_ROBIN - }, "rdata": &schema.Schema{ Type: schema.TypeSet, Set: schema.HashString, @@ -41,10 +35,32 @@ func resourceUltradnsRdpool() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, }, // Optional + "order": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "ROUND_ROBIN", + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value != "ROUND_ROBIN" && value != "FIXED" && value != "RANDOM" { + errors = append(errors, fmt.Errorf( + "Only 'ROUND_ROBIN', 'FIXED', and 'RANDOM' are supported values for 'order'")) + } + return + }, + }, "description": &schema.Schema{ Type: schema.TypeString, Optional: true, - // 0-255 char + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + if v != nil { + value := v.(string) + if len(value) > 255 { + errors = append(errors, fmt.Errorf( + "'description' length must be 0-255")) + } + } + return + }, }, "ttl": &schema.Schema{ Type: schema.TypeInt, @@ -111,8 +127,7 @@ func resourceUltradnsRdpoolRead(d *schema.ResourceData, meta interface{}) error r := rrsets[0] zone := d.Get("zone") - // ttl - d.Set("ttl", r.TTL) + // hostname if r.OwnerName == "" { d.Set("hostname", zone) @@ -134,11 +149,11 @@ func resourceUltradnsRdpoolRead(d *schema.ResourceData, meta interface{}) error } // Set simple values + d.Set("ttl", r.TTL) d.Set("description", p.Description) d.Set("order", p.Order) err = d.Set("rdata", makeSetFromStrings(r.RData)) - //err = d.Set("rdata", makeSetFromRdataAlone(r.RData)) if err != nil { return fmt.Errorf("rdata set failed: %#v", err) } @@ -186,13 +201,12 @@ func resourceUltradnsRdpoolDelete(d *schema.ResourceData, meta interface{}) erro func newRRSetResourceFromRdpool(d *schema.ResourceData) (rRSetResource, error) { //rDataRaw := d.Get("rdata").(*schema.Set).List() r := rRSetResource{ - // "The only valid rrtype value for SiteBacker or Traffic Controller pools is A" + // "The only valid rrtype value for RDpools is A" // per https://portal.ultradns.com/static/docs/REST-API_User_Guide.pdf RRType: "A", Zone: d.Get("zone").(string), OwnerName: d.Get("name").(string), TTL: d.Get("ttl").(int), - //RData: unzipRdataHosts(rDataRaw), } if attr, ok := d.GetOk("rdata"); ok { rdata := attr.(*schema.Set).List() @@ -213,27 +227,3 @@ func newRRSetResourceFromRdpool(d *schema.ResourceData) (rRSetResource, error) { return r, nil } - -// zip RData into []map[string]interface{} -func zipRDataAlone(rds []string) []map[string]interface{} { - result := make([]map[string]interface{}, 0, len(rds)) - for _, rd := range rds { - r := map[string]interface{}{ - // "host": rds[i], - "host": rd, - } - result = append(result, r) - } - return result -} - -// makeSetFromRdatas encodes an array of Rdata into a -// *schema.Set in the appropriate structure for the schema -func makeSetFromRdataAlone(rds []string) *schema.Set { - s := &schema.Set{F: hashRdatas} - rs := zipRDataAlone(rds) - for _, r := range rs { - s.Add(r) - } - return s -} diff --git a/website/source/docs/providers/ultradns/r/rdpool.html.markdown b/website/source/docs/providers/ultradns/r/rdpool.html.markdown index be7410fc1..80f9a3e55 100644 --- a/website/source/docs/providers/ultradns/r/rdpool.html.markdown +++ b/website/source/docs/providers/ultradns/r/rdpool.html.markdown @@ -33,8 +33,8 @@ The following arguments are supported: * `zone` - (Required) The domain to add the record to * `name` - (Required) The name of the record -* `order` - (Required) Ordering rule, one of FIXED, RANDOM or ROUND_ROBIN * `rdata` - (Required) list ip addresses. +* `order` - (Optional) Ordering rule, one of FIXED, RANDOM or ROUND_ROBIN. Default: 'ROUND_ROBIN'. * `description` - (Optional) Description of the Resource Distribution pool. Valid values are strings less than 256 characters. * `ttl` - (Optional) The TTL of the pool in seconds. Default: `3600`. From fe8029e65e3e1d80f5cf2e30a80342f80408d6eb Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Wed, 19 Apr 2017 17:30:58 -0400 Subject: [PATCH 03/89] initial attempt --- .../providers/aws/resource_aws_instance.go | 25 ++++++++- .../aws/resource_aws_instance_test.go | 53 +++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/builtin/providers/aws/resource_aws_instance.go b/builtin/providers/aws/resource_aws_instance.go index 65e348d34..e5d4a128f 100644 --- a/builtin/providers/aws/resource_aws_instance.go +++ b/builtin/providers/aws/resource_aws_instance.go @@ -128,11 +128,20 @@ func resourceAwsInstance() *schema.Resource { Computed: true, }, + // TODO: Deprecate me "network_interface_id": { Type: schema.TypeString, Computed: true, }, + "primary_network_interface": { + ConflictsWith: []string{"associate_public_ip_address", "subnet_id", "private_ip", "vpc_security_group_ids", "security_groups", "ipv6_addresses", "ipv6_address_count"}, + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + "public_ip": { Type: schema.TypeString, Computed: true, @@ -533,7 +542,8 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { for _, ni := range instance.NetworkInterfaces { if *ni.Attachment.DeviceIndex == 0 { d.Set("subnet_id", ni.SubnetId) - d.Set("network_interface_id", ni.NetworkInterfaceId) + d.Set("network_interface_id", ni.NetworkInterfaceId) // TODO: Deprecate me + d.Set("primary_network_interface", ni.NetworkInterfaceId) d.Set("associate_public_ip_address", ni.Association != nil) d.Set("ipv6_address_count", len(ni.Ipv6Addresses)) @@ -544,7 +554,8 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { } } else { d.Set("subnet_id", instance.SubnetId) - d.Set("network_interface_id", "") + d.Set("network_interface_id", "") // TODO: Deprecate me + d.Set("primary_network_interface", "") } if err := d.Set("ipv6_addresses", ipv6Addresses); err != nil { @@ -1260,6 +1271,9 @@ func buildAwsInstanceOpts( } } + // Check if using non-defaullt primary network interface + interface_id, interfaceOk := d.GetOk("primary_network_interface") + if hasSubnet && associatePublicIPAddress { // If we have a non-default VPC / Subnet specified, we can flag // AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided. @@ -1285,6 +1299,13 @@ func buildAwsInstanceOpts( } } + opts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni} + } else if interfaceOk { + ni := &ec2.InstanceNetworkInterfaceSpecification{ + DeviceIndex: aws.Int64(int64(0)), + NetworkInterfaceId: aws.String(interface_id.(string)), + } + opts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni} } else { if subnetID != "" { diff --git a/builtin/providers/aws/resource_aws_instance_test.go b/builtin/providers/aws/resource_aws_instance_test.go index 2b835f6d7..1de8ac768 100644 --- a/builtin/providers/aws/resource_aws_instance_test.go +++ b/builtin/providers/aws/resource_aws_instance_test.go @@ -877,6 +877,27 @@ func TestAccAWSInstance_changeInstanceType(t *testing.T) { }) } +func TestAccAWSInstance_primaryNetworkInterface(t *testing.T) { + var instance ec2.Instance + var ini ec2.NetworkInterface + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccInstanceConfigPrimaryNetworkInterface, + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists("aws_instance.foo", &instance), + testAccCheckAWSENIExists("aws_network_interface.bar", &ini), + resource.TestCheckResourceAttrSet("aws_instance.foo", "primary_network_interface"), + ), + }, + }, + }) +} + func testAccCheckInstanceNotRecreated(t *testing.T, before, after *ec2.Instance) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -1536,3 +1557,35 @@ resource "aws_instance" "foo" { subnet_id = "${aws_subnet.foo.id}" } ` + +const testAccInstanceConfigPrimaryNetworkInterface = ` +resource "aws_vpc" "foo" { + cidr_block = "172.16.0.0/16" + tags { + Name = "tf-instance-test" + } +} + +resource "aws_subnet" "foo" { + vpc_id = "${aws_vpc.foo.id}" + cidr_block = "172.16.10.0/24" + availability_zone = "us-west-2a" + tags { + Name = "tf-instance-test" + } +} + +resource "aws_network_interface" "bar" { + subnet_id = "${aws_subnet.foo.id}" + private_ips = ["172.16.10.100"] + tags { + Name = "primary_network_interface" + } +} + +resource "aws_instance" "foo" { + ami = "ami-22b9a343" + instance_type = "t2.micro" + primary_network_interface = "${aws_network_interface.bar.id}" +} +` From 1a21a388f51ddb759778c264fccb8868ac388e9a Mon Sep 17 00:00:00 2001 From: Richard Clamp Date: Thu, 20 Oct 2016 22:20:47 +0100 Subject: [PATCH 04/89] vendor: bring in github.com/xanzy/go-gitlab 0.5.1 Add go-gitlab as a dependency for the gitlab provider --- .../github.com/xanzy/go-gitlab/CHANGELOG.md | 22 + vendor/github.com/xanzy/go-gitlab/LICENSE | 202 ++++ vendor/github.com/xanzy/go-gitlab/README.md | 125 +++ vendor/github.com/xanzy/go-gitlab/branches.go | 201 ++++ .../xanzy/go-gitlab/build_variables.go | 146 +++ vendor/github.com/xanzy/go-gitlab/builds.go | 349 +++++++ vendor/github.com/xanzy/go-gitlab/commits.go | 444 ++++++++ .../github.com/xanzy/go-gitlab/deploy_keys.go | 150 +++ vendor/github.com/xanzy/go-gitlab/events.go | 557 ++++++++++ vendor/github.com/xanzy/go-gitlab/gitlab.go | 605 +++++++++++ vendor/github.com/xanzy/go-gitlab/groups.go | 358 +++++++ vendor/github.com/xanzy/go-gitlab/issues.go | 264 +++++ vendor/github.com/xanzy/go-gitlab/labels.go | 164 +++ .../xanzy/go-gitlab/merge_requests.go | 333 ++++++ .../github.com/xanzy/go-gitlab/milestones.go | 216 ++++ .../github.com/xanzy/go-gitlab/namespaces.go | 89 ++ vendor/github.com/xanzy/go-gitlab/notes.go | 418 ++++++++ .../xanzy/go-gitlab/notifications.go | 214 ++++ .../github.com/xanzy/go-gitlab/pipelines.go | 191 ++++ .../xanzy/go-gitlab/project_snippets.go | 232 +++++ vendor/github.com/xanzy/go-gitlab/projects.go | 981 ++++++++++++++++++ .../xanzy/go-gitlab/repositories.go | 259 +++++ .../xanzy/go-gitlab/repository_files.go | 210 ++++ vendor/github.com/xanzy/go-gitlab/services.go | 279 +++++ vendor/github.com/xanzy/go-gitlab/session.go | 78 ++ vendor/github.com/xanzy/go-gitlab/settings.go | 117 +++ vendor/github.com/xanzy/go-gitlab/strings.go | 94 ++ .../xanzy/go-gitlab/system_hooks.go | 143 +++ vendor/github.com/xanzy/go-gitlab/tags.go | 149 +++ .../github.com/xanzy/go-gitlab/time_stats.go | 171 +++ vendor/github.com/xanzy/go-gitlab/users.go | 584 +++++++++++ vendor/vendor.json | 6 + 32 files changed, 8351 insertions(+) create mode 100644 vendor/github.com/xanzy/go-gitlab/CHANGELOG.md create mode 100644 vendor/github.com/xanzy/go-gitlab/LICENSE create mode 100644 vendor/github.com/xanzy/go-gitlab/README.md create mode 100644 vendor/github.com/xanzy/go-gitlab/branches.go create mode 100644 vendor/github.com/xanzy/go-gitlab/build_variables.go create mode 100644 vendor/github.com/xanzy/go-gitlab/builds.go create mode 100644 vendor/github.com/xanzy/go-gitlab/commits.go create mode 100644 vendor/github.com/xanzy/go-gitlab/deploy_keys.go create mode 100644 vendor/github.com/xanzy/go-gitlab/events.go create mode 100644 vendor/github.com/xanzy/go-gitlab/gitlab.go create mode 100644 vendor/github.com/xanzy/go-gitlab/groups.go create mode 100644 vendor/github.com/xanzy/go-gitlab/issues.go create mode 100644 vendor/github.com/xanzy/go-gitlab/labels.go create mode 100644 vendor/github.com/xanzy/go-gitlab/merge_requests.go create mode 100644 vendor/github.com/xanzy/go-gitlab/milestones.go create mode 100644 vendor/github.com/xanzy/go-gitlab/namespaces.go create mode 100644 vendor/github.com/xanzy/go-gitlab/notes.go create mode 100644 vendor/github.com/xanzy/go-gitlab/notifications.go create mode 100644 vendor/github.com/xanzy/go-gitlab/pipelines.go create mode 100644 vendor/github.com/xanzy/go-gitlab/project_snippets.go create mode 100644 vendor/github.com/xanzy/go-gitlab/projects.go create mode 100644 vendor/github.com/xanzy/go-gitlab/repositories.go create mode 100644 vendor/github.com/xanzy/go-gitlab/repository_files.go create mode 100644 vendor/github.com/xanzy/go-gitlab/services.go create mode 100644 vendor/github.com/xanzy/go-gitlab/session.go create mode 100644 vendor/github.com/xanzy/go-gitlab/settings.go create mode 100644 vendor/github.com/xanzy/go-gitlab/strings.go create mode 100644 vendor/github.com/xanzy/go-gitlab/system_hooks.go create mode 100644 vendor/github.com/xanzy/go-gitlab/tags.go create mode 100644 vendor/github.com/xanzy/go-gitlab/time_stats.go create mode 100644 vendor/github.com/xanzy/go-gitlab/users.go diff --git a/vendor/github.com/xanzy/go-gitlab/CHANGELOG.md b/vendor/github.com/xanzy/go-gitlab/CHANGELOG.md new file mode 100644 index 000000000..09cb6db4d --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/CHANGELOG.md @@ -0,0 +1,22 @@ +go-github CHANGELOG +=================== + +0.4.0 +----- +- Add support to use [`sudo`](https://docs.gitlab.com/ce/api/README.html#sudo) for all API calls. +- Add support for the Notification Settings API. +- Add support for the Time Tracking API. +- Make sure that the error response correctly outputs any returned errors. +- And a reasonable number of smaller enhanchements and bugfixes. + +0.3.0 +----- +- Moved the tags related API calls to their own service, following the Gitlab API structure. + +0.2.0 +----- +- Convert all Option structs to use pointers for their fields. + +0.1.0 +----- +- Initial release. diff --git a/vendor/github.com/xanzy/go-gitlab/LICENSE b/vendor/github.com/xanzy/go-gitlab/LICENSE new file mode 100644 index 000000000..e06d20818 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/LICENSE @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. + diff --git a/vendor/github.com/xanzy/go-gitlab/README.md b/vendor/github.com/xanzy/go-gitlab/README.md new file mode 100644 index 000000000..7b6d1bcfd --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/README.md @@ -0,0 +1,125 @@ +# go-gitlab + +A GitLab API client enabling Go programs to interact with GitLab in a simple and uniform way + +**Documentation:** [![GoDoc](https://godoc.org/github.com/xanzy/go-gitlab?status.svg)](https://godoc.org/github.com/xanzy/go-gitlab) +**Build Status:** [![Build Status](https://travis-ci.org/xanzy/go-gitlab.svg?branch=master)](https://travis-ci.org/xanzy/go-gitlab) + +## NOTE + +Release v0.5.0 (released on 22-03-2017) no longer supports Go versions older +then 1.7.x If you want (or need) to use an older Go version please use v0.4.1 + +## Coverage + +This API client package covers **100%** of the existing GitLab API calls! So this +includes all calls to the following services: + +- [x] Users +- [x] Session +- [x] Projects (including setting Webhooks) +- [x] Project Snippets +- [x] Services +- [x] Repositories +- [x] Repository Files +- [x] Commits +- [x] Branches +- [x] Merge Requests +- [x] Issues +- [x] Labels +- [x] Milestones +- [x] Notes (comments) +- [x] Deploy Keys +- [x] System Hooks +- [x] Groups +- [x] Namespaces +- [x] Settings +- [x] Pipelines + +## Usage + +```go +import "github.com/xanzy/go-gitlab" +``` + +Construct a new GitLab client, then use the various services on the client to +access different parts of the GitLab API. For example, to list all +users: + +```go +git := gitlab.NewClient(nil, "yourtokengoeshere") +//git.SetBaseURL("https://git.mydomain.com/api/v3") +users, _, err := git.Users.ListUsers() +``` + +Some API methods have optional parameters that can be passed. For example, +to list all projects for user "svanharmelen": + +```go +git := gitlab.NewClient(nil) +opt := &ListProjectsOptions{Search: gitlab.String("svanharmelen")}) +projects, _, err := git.Projects.ListProjects(opt) +``` + +### Examples + +The [examples](https://github.com/xanzy/go-gitlab/tree/master/examples) directory +contains a couple for clear examples, of which one is partially listed here as well: + +```go +package main + +import ( + "log" + + "github.com/xanzy/go-gitlab" +) + +func main() { + git := gitlab.NewClient(nil, "yourtokengoeshere") + + // Create new project + p := &gitlab.CreateProjectOptions{ + Name: gitlab.String("My Project"), + Description: gitlab.String("Just a test project to play with"), + MergeRequestsEnabled: gitlab.Bool(true), + SnippetsEnabled: gitlab.Bool(true), + VisibilityLevel: gitlab.VisibilityLevel(gitlab.PublicVisibility), + } + project, _, err := git.Projects.CreateProject(p) + if err != nil { + log.Fatal(err) + } + + // Add a new snippet + s := &gitlab.CreateSnippetOptions{ + Title: gitlab.String("Dummy Snippet"), + FileName: gitlab.String("snippet.go"), + Code: gitlab.String("package main...."), + VisibilityLevel: gitlab.VisibilityLevel(gitlab.PublicVisibility), + } + _, _, err = git.ProjectSnippets.CreateSnippet(project.ID, s) + if err != nil { + log.Fatal(err) + } +} + +``` + +For complete usage of go-gitlab, see the full [package docs](https://godoc.org/github.com/xanzy/go-gitlab). + +## ToDo + +- The biggest thing this package still needs is tests :disappointed: + +## Issues + +- If you have an issue: report it on the [issue tracker](https://github.com/xanzy/go-gitlab/issues) + +## Author + +Sander van Harmelen () + +## License + +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 diff --git a/vendor/github.com/xanzy/go-gitlab/branches.go b/vendor/github.com/xanzy/go-gitlab/branches.go new file mode 100644 index 000000000..adb4c739b --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/branches.go @@ -0,0 +1,201 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// BranchesService handles communication with the branch related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/branches.html +type BranchesService struct { + client *Client +} + +// Branch represents a GitLab branch. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/branches.html +type Branch struct { + Commit *Commit `json:"commit"` + Name string `json:"name"` + Protected bool `json:"protected"` +} + +func (b Branch) String() string { + return Stringify(b) +} + +// ListBranches gets a list of repository branches from a project, sorted by +// name alphabetically. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/branches.html#list-repository-branches +func (s *BranchesService) ListBranches(pid interface{}, options ...OptionFunc) ([]*Branch, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/branches", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var b []*Branch + resp, err := s.client.Do(req, &b) + if err != nil { + return nil, resp, err + } + + return b, resp, err +} + +// GetBranch gets a single project repository branch. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/branches.html#get-single-repository-branch +func (s *BranchesService) GetBranch(pid interface{}, branch string, options ...OptionFunc) (*Branch, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/branches/%s", url.QueryEscape(project), branch) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + b := new(Branch) + resp, err := s.client.Do(req, b) + if err != nil { + return nil, resp, err + } + + return b, resp, err +} + +// ProtectBranch protects a single project repository branch. This is an +// idempotent function, protecting an already protected repository branch +// still returns a 200 OK status code. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/branches.html#protect-repository-branch +func (s *BranchesService) ProtectBranch(pid interface{}, branch string, options ...OptionFunc) (*Branch, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/branches/%s/protect", url.QueryEscape(project), branch) + + req, err := s.client.NewRequest("PUT", u, nil, options) + if err != nil { + return nil, nil, err + } + + b := new(Branch) + resp, err := s.client.Do(req, b) + if err != nil { + return nil, resp, err + } + + return b, resp, err +} + +// UnprotectBranch unprotects a single project repository branch. This is an +// idempotent function, unprotecting an already unprotected repository branch +// still returns a 200 OK status code. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/branches.html#unprotect-repository-branch +func (s *BranchesService) UnprotectBranch(pid interface{}, branch string, options ...OptionFunc) (*Branch, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/branches/%s/unprotect", url.QueryEscape(project), branch) + + req, err := s.client.NewRequest("PUT", u, nil, options) + if err != nil { + return nil, nil, err + } + + b := new(Branch) + resp, err := s.client.Do(req, b) + if err != nil { + return nil, resp, err + } + + return b, resp, err +} + +// CreateBranchOptions represents the available CreateBranch() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/branches.html#create-repository-branch +type CreateBranchOptions struct { + BranchName *string `url:"branch_name,omitempty" json:"branch_name,omitempty"` + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` +} + +// CreateBranch creates branch from commit SHA or existing branch. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/branches.html#create-repository-branch +func (s *BranchesService) CreateBranch(pid interface{}, opt *CreateBranchOptions, options ...OptionFunc) (*Branch, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/branches", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + b := new(Branch) + resp, err := s.client.Do(req, b) + if err != nil { + return nil, resp, err + } + + return b, resp, err +} + +// DeleteBranch deletes an existing branch. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/branches.html#delete-repository-branch +func (s *BranchesService) DeleteBranch(pid interface{}, branch string, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/repository/branches/%s", url.QueryEscape(project), branch) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/vendor/github.com/xanzy/go-gitlab/build_variables.go b/vendor/github.com/xanzy/go-gitlab/build_variables.go new file mode 100644 index 000000000..a1bdf7f65 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/build_variables.go @@ -0,0 +1,146 @@ +package gitlab + +import ( + "fmt" + "net/url" +) + +// BuildVariablesService handles communication with the project variables related methods +// of the Gitlab API +// +// Gitlab API Docs : https://docs.gitlab.com/ce/api/build_variables.html +type BuildVariablesService struct { + client *Client +} + +// BuildVariable represents a variable available for each build of the given project +// +// Gitlab API Docs : https://docs.gitlab.com/ce/api/build_variables.html +type BuildVariable struct { + Key string `json:"key"` + Value string `json:"value"` +} + +func (v BuildVariable) String() string { + return Stringify(v) +} + +// ListBuildVariables gets the a list of project variables in a project +// +// Gitlab API Docs: +// https://docs.gitlab.com/ce/api/build_variables.html#list-project-variables +func (s *BuildVariablesService) ListBuildVariables(pid interface{}, options ...OptionFunc) ([]*BuildVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/variables", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var v []*BuildVariable + resp, err := s.client.Do(req, &v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} + +// GetBuildVariable gets a single project variable of a project +// +// Gitlab API Docs: +// https://docs.gitlab.com/ce/api/build_variables.html#show-variable-details +func (s *BuildVariablesService) GetBuildVariable(pid interface{}, key string, options ...OptionFunc) (*BuildVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/variables/%s", url.QueryEscape(project), key) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + v := new(BuildVariable) + resp, err := s.client.Do(req, v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} + +// CreateBuildVariable creates a variable for a given project +// +// Gitlab API Docs: +// https://docs.gitlab.com/ce/api/build_variables.html#create-variable +func (s *BuildVariablesService) CreateBuildVariable(pid interface{}, key, value string, options ...OptionFunc) (*BuildVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/variables", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, BuildVariable{key, value}, options) + if err != nil { + return nil, nil, err + } + + v := new(BuildVariable) + resp, err := s.client.Do(req, v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} + +// UpdateBuildVariable updates an existing project variable +// The variable key must exist +// +// Gitlab API Docs: +// https://docs.gitlab.com/ce/api/build_variables.html#update-variable +func (s *BuildVariablesService) UpdateBuildVariable(pid interface{}, key, value string, options ...OptionFunc) (*BuildVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/variables/%s", url.QueryEscape(project), key) + + req, err := s.client.NewRequest("PUT", u, BuildVariable{key, value}, options) + if err != nil { + return nil, nil, err + } + + v := new(BuildVariable) + resp, err := s.client.Do(req, v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} + +// RemoveBuildVariable removes a project variable of a given project identified by its key +// +// Gitlab API Docs: +// https://docs.gitlab.com/ce/api/build_variables.html#remove-variable +func (s *BuildVariablesService) RemoveBuildVariable(pid interface{}, key string, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/variables/%s", url.QueryEscape(project), key) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/vendor/github.com/xanzy/go-gitlab/builds.go b/vendor/github.com/xanzy/go-gitlab/builds.go new file mode 100644 index 000000000..dfee85b32 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/builds.go @@ -0,0 +1,349 @@ +// +// Copyright 2016, Arkbriar +// +// 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 gitlab + +import ( + "bytes" + "fmt" + "io" + "net/url" + "time" +) + +// ListBuildsOptions are options for two list apis +type ListBuildsOptions struct { + ListOptions + Scope []BuildState `url:"scope,omitempty" json:"scope,omitempty"` +} + +// BuildsService handles communication with the ci builds related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/builds.html +type BuildsService struct { + client *Client +} + +// Build represents a ci build. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/builds.html +type Build struct { + Commit *Commit `json:"commit"` + CreatedAt *time.Time `json:"created_at"` + ArtifactsFile struct { + Filename string `json:"filename"` + Size int `json:"size"` + } `json:"artifacts_file"` + FinishedAt *time.Time `json:"finished_at"` + ID int `json:"id"` + Name string `json:"name"` + Ref string `json:"ref"` + Runner struct { + ID int `json:"id"` + Description string `json:"description"` + Active bool `json:"active"` + IsShared bool `json:"is_shared"` + Name string `json:"name"` + } `json:"runner"` + Stage string `json:"stage"` + StartedAt *time.Time `json:"started_at"` + Status string `json:"status"` + Tag bool `json:"tag"` + User *User `json:"user"` +} + +// ListProjectBuilds gets a list of builds in a project. +// +// The scope of builds to show, one or array of: pending, running, +// failed, success, canceled; showing all builds if none provided. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/builds.html#list-project-builds +func (s *BuildsService) ListProjectBuilds(pid interface{}, opts *ListBuildsOptions, options ...OptionFunc) ([]Build, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/builds", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opts, options) + if err != nil { + return nil, nil, err + } + + var builds []Build + resp, err := s.client.Do(req, &builds) + if err != nil { + return nil, resp, err + } + + return builds, resp, err +} + +// ListCommitBuilds gets a list of builds for specific commit in a +// project. If the commit SHA is not found, it will respond with 404. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/builds.html#list-commit-builds +func (s *BuildsService) ListCommitBuilds(pid interface{}, sha string, opts *ListBuildsOptions, options ...OptionFunc) ([]Build, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/builds", project, sha) + + req, err := s.client.NewRequest("GET", u, opts, options) + if err != nil { + return nil, nil, err + } + + var builds []Build + resp, err := s.client.Do(req, &builds) + if err != nil { + return nil, resp, err + } + + return builds, resp, err +} + +// GetBuild gets a single build of a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/builds.html#get-a-single-build +func (s *BuildsService) GetBuild(pid interface{}, buildID int, options ...OptionFunc) (*Build, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/builds/%d", project, buildID) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + build := new(Build) + resp, err := s.client.Do(req, build) + if err != nil { + return nil, resp, err + } + + return build, resp, err +} + +// GetBuildArtifacts get builds artifacts of a project +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/builds.html#get-build-artifacts +func (s *BuildsService) GetBuildArtifacts(pid interface{}, buildID int, options ...OptionFunc) (io.Reader, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/builds/%d/artifacts", project, buildID) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + artifactsBuf := new(bytes.Buffer) + resp, err := s.client.Do(req, artifactsBuf) + if err != nil { + return nil, resp, err + } + + return artifactsBuf, resp, err +} + +// DownloadArtifactsFile download the artifacts file from the given +// reference name and job provided the build finished successfully. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/builds.html#download-the-artifacts-file +func (s *BuildsService) DownloadArtifactsFile(pid interface{}, refName string, job string, options ...OptionFunc) (io.Reader, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/builds/artifacts/%s/download?job=%s", project, refName, job) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + artifactsBuf := new(bytes.Buffer) + resp, err := s.client.Do(req, artifactsBuf) + if err != nil { + return nil, resp, err + } + + return artifactsBuf, resp, err +} + +// GetTraceFile gets a trace of a specific build of a project +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/builds.html#get-a-trace-file +func (s *BuildsService) GetTraceFile(pid interface{}, buildID int, options ...OptionFunc) (io.Reader, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/builds/%d/trace", project, buildID) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + traceBuf := new(bytes.Buffer) + resp, err := s.client.Do(req, traceBuf) + if err != nil { + return nil, resp, err + } + + return traceBuf, resp, err +} + +// CancelBuild cancels a single build of a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/builds.html#cancel-a-build +func (s *BuildsService) CancelBuild(pid interface{}, buildID int, options ...OptionFunc) (*Build, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/builds/%d/cancel", project, buildID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + build := new(Build) + resp, err := s.client.Do(req, build) + if err != nil { + return nil, resp, err + } + + return build, resp, err +} + +// RetryBuild retries a single build of a project +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/builds.html#retry-a-build +func (s *BuildsService) RetryBuild(pid interface{}, buildID int, options ...OptionFunc) (*Build, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/builds/%d/retry", project, buildID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + build := new(Build) + resp, err := s.client.Do(req, build) + if err != nil { + return nil, resp, err + } + + return build, resp, err +} + +// EraseBuild erases a single build of a project, removes a build +// artifacts and a build trace. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/builds.html#erase-a-build +func (s *BuildsService) EraseBuild(pid interface{}, buildID int, options ...OptionFunc) (*Build, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/builds/%d/erase", project, buildID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + build := new(Build) + resp, err := s.client.Do(req, build) + if err != nil { + return nil, resp, err + } + + return build, resp, err +} + +// KeepArtifacts prevents artifacts from being deleted when +// expiration is set. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/builds.html#keep-artifacts +func (s *BuildsService) KeepArtifacts(pid interface{}, buildID int, options ...OptionFunc) (*Build, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/builds/%d/artifacts/keep", project, buildID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + build := new(Build) + resp, err := s.client.Do(req, build) + if err != nil { + return nil, resp, err + } + + return build, resp, err +} + +// PlayBuild triggers a nanual action to start a build. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/builds.html#play-a-build +func (s *BuildsService) PlayBuild(pid interface{}, buildID int, options ...OptionFunc) (*Build, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/builds/%d/play", project, buildID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + build := new(Build) + resp, err := s.client.Do(req, build) + if err != nil { + return nil, resp, err + } + + return build, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/commits.go b/vendor/github.com/xanzy/go-gitlab/commits.go new file mode 100644 index 000000000..7aa642c52 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/commits.go @@ -0,0 +1,444 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// CommitsService handles communication with the commit related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html +type CommitsService struct { + client *Client +} + +// Commit represents a GitLab commit. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html +type Commit struct { + ID string `json:"id"` + ShortID string `json:"short_id"` + Title string `json:"title"` + AuthorName string `json:"author_name"` + AuthorEmail string `json:"author_email"` + AuthoredDate *time.Time `json:"authored_date"` + CommitterName string `json:"committer_name"` + CommitterEmail string `json:"committer_email"` + CommittedDate *time.Time `json:"committed_date"` + CreatedAt *time.Time `json:"created_at"` + Message string `json:"message"` + ParentIDs []string `json:"parent_ids"` + Stats *CommitStats `json:"stats"` + Status *BuildState `json:"status"` +} + +// CommitStats represents the number of added and deleted files in a commit. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html +type CommitStats struct { + Additions int `json:"additions"` + Deletions int `json:"deletions"` + Total int `json:"total"` +} + +func (c Commit) String() string { + return Stringify(c) +} + +// ListCommitsOptions represents the available ListCommits() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#list-repository-commits +type ListCommitsOptions struct { + ListOptions + RefName *string `url:"ref_name,omitempty" json:"ref_name,omitempty"` + Since time.Time `url:"since,omitempty" json:"since,omitempty"` + Until time.Time `url:"until,omitempty" json:"until,omitempty"` +} + +// ListCommits gets a list of repository commits in a project. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#list-commits +func (s *CommitsService) ListCommits(pid interface{}, opt *ListCommitsOptions, options ...OptionFunc) ([]*Commit, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var c []*Commit + resp, err := s.client.Do(req, &c) + if err != nil { + return nil, resp, err + } + + return c, resp, err +} + +// FileAction represents the available actions that can be performed on a file. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions +type FileAction string + +// The available file actions. +const ( + FileCreate FileAction = "create" + FileDelete FileAction = "delete" + FileMove FileAction = "move" + FileUpdate FileAction = "update" +) + +// CommitAction represents a single file action within a commit. +type CommitAction struct { + Action FileAction `url:"action" json:"action,omitempty"` + FilePath string `url:"file_path" json:"file_path,omitempty"` + PreviousPath string `url:"previous_path,omitempty" json:"previous_path,omitempty"` + Content string `url:"content,omitempty" json:"content,omitempty"` + Encoding string `url:"encoding,omitempty" json:"encoding,omitempty"` +} + +// CreateCommitOptions represents the available options for a new commit. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions +type CreateCommitOptions struct { + BranchName *string `url:"branch_name" json:"branch_name,omitempty"` + CommitMessage *string `url:"commit_message" json:"commit_message,omitempty"` + Actions []*CommitAction `url:"actions" json:"actions,omitempty"` + AuthorEmail *string `url:"author_email,omitempty" json:"author_email,omitempty"` + AuthorName *string `url:"author_name,omitempty" json:"author_name,omitempty"` +} + +// CreateCommit creates a commit with multiple files and actions. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions +func (s *CommitsService) CreateCommit(pid interface{}, opt *CreateCommitOptions, options ...OptionFunc) (*Commit, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + var c *Commit + resp, err := s.client.Do(req, &c) + if err != nil { + return nil, resp, err + } + + return c, resp, err +} + +// GetCommit gets a specific commit identified by the commit hash or name of a +// branch or tag. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#get-a-single-commit +func (s *CommitsService) GetCommit(pid interface{}, sha string, options ...OptionFunc) (*Commit, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s", url.QueryEscape(project), sha) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + c := new(Commit) + resp, err := s.client.Do(req, c) + if err != nil { + return nil, resp, err + } + + return c, resp, err +} + +// Diff represents a GitLab diff. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html +type Diff struct { + Diff string `json:"diff"` + NewPath string `json:"new_path"` + OldPath string `json:"old_path"` + AMode string `json:"a_mode"` + BMode string `json:"b_mode"` + NewFile bool `json:"new_file"` + RenamedFile bool `json:"renamed_file"` + DeletedFile bool `json:"deleted_file"` +} + +func (d Diff) String() string { + return Stringify(d) +} + +// GetCommitDiff gets the diff of a commit in a project.. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/commits.html#get-the-diff-of-a-commit +func (s *CommitsService) GetCommitDiff(pid interface{}, sha string, options ...OptionFunc) ([]*Diff, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/diff", url.QueryEscape(project), sha) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var d []*Diff + resp, err := s.client.Do(req, &d) + if err != nil { + return nil, resp, err + } + + return d, resp, err +} + +// CommitComment represents a GitLab commit comment. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html +type CommitComment struct { + Note string `json:"note"` + Path string `json:"path"` + Line int `json:"line"` + LineType string `json:"line_type"` + Author Author `json:"author"` +} + +// Author represents a GitLab commit author +type Author struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + Blocked bool `json:"blocked"` + CreatedAt *time.Time `json:"created_at"` +} + +func (c CommitComment) String() string { + return Stringify(c) +} + +// GetCommitComments gets the comments of a commit in a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/commits.html#get-the-comments-of-a-commit +func (s *CommitsService) GetCommitComments(pid interface{}, sha string, options ...OptionFunc) ([]*CommitComment, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/comments", url.QueryEscape(project), sha) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var c []*CommitComment + resp, err := s.client.Do(req, &c) + if err != nil { + return nil, resp, err + } + + return c, resp, err +} + +// PostCommitCommentOptions represents the available PostCommitComment() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/commits.html#post-comment-to-commit +type PostCommitCommentOptions struct { + Note *string `url:"note,omitempty" json:"note,omitempty"` + Path *string `url:"path" json:"path"` + Line *int `url:"line" json:"line"` + LineType *string `url:"line_type" json:"line_type"` +} + +// PostCommitComment adds a comment to a commit. Optionally you can post +// comments on a specific line of a commit. Therefor both path, line_new and +// line_old are required. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/commits.html#post-comment-to-commit +func (s *CommitsService) PostCommitComment(pid interface{}, sha string, opt *PostCommitCommentOptions, options ...OptionFunc) (*CommitComment, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/comments", url.QueryEscape(project), sha) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + c := new(CommitComment) + resp, err := s.client.Do(req, c) + if err != nil { + return nil, resp, err + } + + return c, resp, err +} + +// GetCommitStatusesOptions represents the available GetCommitStatuses() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#get-the-status-of-a-commit +type GetCommitStatusesOptions struct { + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` + Stage *string `url:"stage,omitempty" json:"stage,omitempty"` + Name *string `url:"name,omitempty" json:"name,omitempty"` + All *bool `url:"all,omitempty" json:"all,omitempty"` +} + +// CommitStatus represents a GitLab commit status. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#get-the-status-of-a-commit +type CommitStatus struct { + ID int `json:"id"` + SHA string `json:"sha"` + Ref string `json:"ref"` + Status string `json:"status"` + Name string `json:"name"` + TargetURL string `json:"target_url"` + Description string `json:"description"` + CreatedAt *time.Time `json:"created_at"` + StartedAt *time.Time `json:"started_at"` + FinishedAt *time.Time `json:"finished_at"` + Author Author `json:"author"` +} + +// GetCommitStatuses gets the statuses of a commit in a project. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#get-the-status-of-a-commit +func (s *CommitsService) GetCommitStatuses(pid interface{}, sha string, opt *GetCommitStatusesOptions, options ...OptionFunc) ([]*CommitStatus, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/statuses", url.QueryEscape(project), sha) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var cs []*CommitStatus + resp, err := s.client.Do(req, &cs) + if err != nil { + return nil, resp, err + } + + return cs, resp, err +} + +// SetCommitStatusOptions represents the available SetCommitStatus() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#post-the-status-to-commit +type SetCommitStatusOptions struct { + State BuildState `url:"state" json:"state"` + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` + Name *string `url:"name,omitempty" json:"name,omitempty"` + Context *string `url:"context,omitempty" json:"context,omitempty"` + TargetURL *string `url:"target_url,omitempty" json:"target_url,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` +} + +// BuildState represents a GitLab build state. +type BuildState string + +// These constants represent all valid build states. +const ( + Pending BuildState = "pending" + Running BuildState = "running" + Success BuildState = "success" + Failed BuildState = "failed" + Canceled BuildState = "canceled" +) + +// SetCommitStatus sets the status of a commit in a project. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#post-the-status-to-commit +func (s *CommitsService) SetCommitStatus(pid interface{}, sha string, opt *SetCommitStatusOptions, options ...OptionFunc) (*CommitStatus, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/statuses/%s", url.QueryEscape(project), sha) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + var cs *CommitStatus + resp, err := s.client.Do(req, &cs) + if err != nil { + return nil, resp, err + } + + return cs, resp, err +} + +// CherryPickCommitOptions represents the available options for cherry-picking a commit. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#cherry-pick-a-commit +type CherryPickCommitOptions struct { + TargetBranch *string `url:"branch" json:"branch,omitempty"` +} + +// CherryPickCommit sherry picks a commit to a given branch. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#cherry-pick-a-commit +func (s *CommitsService) CherryPickCommit(pid interface{}, sha string, opt *CherryPickCommitOptions, options ...OptionFunc) (*Commit, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/cherry_pick", + url.QueryEscape(project), url.QueryEscape(sha)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + var c *Commit + resp, err := s.client.Do(req, &c) + if err != nil { + return nil, resp, err + } + + return c, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/deploy_keys.go b/vendor/github.com/xanzy/go-gitlab/deploy_keys.go new file mode 100644 index 000000000..1e9eae857 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/deploy_keys.go @@ -0,0 +1,150 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// DeployKeysService handles communication with the keys related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/deploy_keys.html +type DeployKeysService struct { + client *Client +} + +// DeployKey represents a GitLab deploy key. +type DeployKey struct { + ID int `json:"id"` + Title string `json:"title"` + Key string `json:"key"` + CanPush *bool `json:"can_push"` + CreatedAt *time.Time `json:"created_at"` +} + +func (k DeployKey) String() string { + return Stringify(k) +} + +// ListDeployKeys gets a list of a project's deploy keys +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/deploy_keys.html#list-deploy-keys +func (s *DeployKeysService) ListDeployKeys(pid interface{}, options ...OptionFunc) ([]*DeployKey, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/keys", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var k []*DeployKey + resp, err := s.client.Do(req, &k) + if err != nil { + return nil, resp, err + } + + return k, resp, err +} + +// GetDeployKey gets a single deploy key. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/deploy_keys.html#single-deploy-key +func (s *DeployKeysService) GetDeployKey(pid interface{}, deployKey int, options ...OptionFunc) (*DeployKey, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/keys/%d", url.QueryEscape(project), deployKey) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + k := new(DeployKey) + resp, err := s.client.Do(req, k) + if err != nil { + return nil, resp, err + } + + return k, resp, err +} + +// AddDeployKeyOptions represents the available ADDDeployKey() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/deploy_keys.html#add-deploy-key +type AddDeployKeyOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + Key *string `url:"key,omitempty" json:"key,omitempty"` + CanPush *bool `url:"can_push,omitempty" json:"can_push,omitempty"` +} + +// AddDeployKey creates a new deploy key for a project. If deploy key already +// exists in another project - it will be joined to project but only if +// original one was is accessible by same user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/deploy_keys.html#add-deploy-key +func (s *DeployKeysService) AddDeployKey(pid interface{}, opt *AddDeployKeyOptions, options ...OptionFunc) (*DeployKey, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/keys", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + k := new(DeployKey) + resp, err := s.client.Do(req, k) + if err != nil { + return nil, resp, err + } + + return k, resp, err +} + +// DeleteDeployKey deletes a deploy key from a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/deploy_keys.html#delete-deploy-key +func (s *DeployKeysService) DeleteDeployKey(pid interface{}, deployKey int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/keys/%d", url.QueryEscape(project), deployKey) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/vendor/github.com/xanzy/go-gitlab/events.go b/vendor/github.com/xanzy/go-gitlab/events.go new file mode 100644 index 000000000..ef334592e --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/events.go @@ -0,0 +1,557 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import "time" + +// PushEvent represents a push event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#push-events +type PushEvent struct { + ObjectKind string `json:"object_kind"` + Before string `json:"before"` + After string `json:"after"` + Ref string `json:"ref"` + CheckoutSha string `json:"checkout_sha"` + UserID int `json:"user_id"` + UserName string `json:"user_name"` + UserEmail string `json:"user_email"` + UserAvatar string `json:"user_avatar"` + ProjectID int `json:"project_id"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + VisibilityLevel VisibilityLevelValue `json:"visibility_level"` + } `json:"project"` + Repository *Repository `json:"repository"` + Commits []*Commit `json:"commits"` + TotalCommitsCount int `json:"total_commits_count"` +} + +// TagEvent represents a tag event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#tag-events +type TagEvent struct { + ObjectKind string `json:"object_kind"` + Before string `json:"before"` + After string `json:"after"` + Ref string `json:"ref"` + CheckoutSha string `json:"checkout_sha"` + UserID int `json:"user_id"` + UserName string `json:"user_name"` + UserAvatar string `json:"user_avatar"` + ProjectID int `json:"project_id"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + VisibilityLevel VisibilityLevelValue `json:"visibility_level"` + } `json:"project"` + Repository *Repository `json:"repository"` + Commits []*Commit `json:"commits"` + TotalCommitsCount int `json:"total_commits_count"` +} + +// IssueEvent represents a issue event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#issues-events +type IssueEvent struct { + ObjectKind string `json:"object_kind"` + User *User `json:"user"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + VisibilityLevel VisibilityLevelValue `json:"visibility_level"` + } `json:"project"` + Repository *Repository `json:"repository"` + ObjectAttributes struct { + ID int `json:"id"` + Title string `json:"title"` + AssigneeID int `json:"assignee_id"` + AuthorID int `json:"author_id"` + ProjectID int `json:"project_id"` + CreatedAt string `json:"created_at"` // Should be *time.Time (see Gitlab issue #21468) + UpdatedAt string `json:"updated_at"` // Should be *time.Time (see Gitlab issue #21468) + Position int `json:"position"` + BranchName string `json:"branch_name"` + Description string `json:"description"` + MilestoneID int `json:"milestone_id"` + State string `json:"state"` + Iid int `json:"iid"` + URL string `json:"url"` + Action string `json:"action"` + } `json:"object_attributes"` + Assignee struct { + Name string `json:"name"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + } `json:"assignee"` +} + +// CommitCommentEvent represents a comment on a commit event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#comment-on-commit +type CommitCommentEvent struct { + ObjectKind string `json:"object_kind"` + User *User `json:"user"` + ProjectID int `json:"project_id"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + VisibilityLevel VisibilityLevelValue `json:"visibility_level"` + } `json:"project"` + Repository *Repository `json:"repository"` + ObjectAttributes struct { + ID int `json:"id"` + Note string `json:"note"` + NoteableType string `json:"noteable_type"` + AuthorID int `json:"author_id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ProjectID int `json:"project_id"` + Attachment string `json:"attachment"` + LineCode string `json:"line_code"` + CommitID string `json:"commit_id"` + NoteableID int `json:"noteable_id"` + System bool `json:"system"` + StDiff struct { + Diff string `json:"diff"` + NewPath string `json:"new_path"` + OldPath string `json:"old_path"` + AMode string `json:"a_mode"` + BMode string `json:"b_mode"` + NewFile bool `json:"new_file"` + RenamedFile bool `json:"renamed_file"` + DeletedFile bool `json:"deleted_file"` + } `json:"st_diff"` + } `json:"object_attributes"` + Commit *Commit `json:"commit"` +} + +// MergeCommentEvent represents a comment on a merge event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#comment-on-merge-request +type MergeCommentEvent struct { + ObjectKind string `json:"object_kind"` + User *User `json:"user"` + ProjectID int `json:"project_id"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + VisibilityLevel VisibilityLevelValue `json:"visibility_level"` + } `json:"project"` + Repository *Repository `json:"repository"` + ObjectAttributes struct { + ID int `json:"id"` + Note string `json:"note"` + NoteableType string `json:"noteable_type"` + AuthorID int `json:"author_id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ProjectID int `json:"project_id"` + Attachment string `json:"attachment"` + LineCode string `json:"line_code"` + CommitID string `json:"commit_id"` + NoteableID int `json:"noteable_id"` + System bool `json:"system"` + StDiff *Diff `json:"st_diff"` + URL string `json:"url"` + } `json:"object_attributes"` + MergeRequest *MergeRequest `json:"merge_request"` +} + +// IssueCommentEvent represents a comment on an issue event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#comment-on-issue +type IssueCommentEvent struct { + ObjectKind string `json:"object_kind"` + User *User `json:"user"` + ProjectID int `json:"project_id"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + VisibilityLevel VisibilityLevelValue `json:"visibility_level"` + } `json:"project"` + Repository *Repository `json:"repository"` + ObjectAttributes struct { + ID int `json:"id"` + Note string `json:"note"` + NoteableType string `json:"noteable_type"` + AuthorID int `json:"author_id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ProjectID int `json:"project_id"` + Attachment string `json:"attachment"` + LineCode string `json:"line_code"` + CommitID string `json:"commit_id"` + NoteableID int `json:"noteable_id"` + System bool `json:"system"` + StDiff []*Diff `json:"st_diff"` + URL string `json:"url"` + } `json:"object_attributes"` + Issue *Issue `json:"issue"` +} + +// SnippetCommentEvent represents a comment on a snippet event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#comment-on-code-snippet +type SnippetCommentEvent struct { + ObjectKind string `json:"object_kind"` + User *User `json:"user"` + ProjectID int `json:"project_id"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + VisibilityLevel VisibilityLevelValue `json:"visibility_level"` + } `json:"project"` + Repository *Repository `json:"repository"` + ObjectAttributes struct { + ID int `json:"id"` + Note string `json:"note"` + NoteableType string `json:"noteable_type"` + AuthorID int `json:"author_id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ProjectID int `json:"project_id"` + Attachment string `json:"attachment"` + LineCode string `json:"line_code"` + CommitID string `json:"commit_id"` + NoteableID int `json:"noteable_id"` + System bool `json:"system"` + StDiff *Diff `json:"st_diff"` + URL string `json:"url"` + } `json:"object_attributes"` + Snippet *Snippet `json:"snippet"` +} + +// MergeEvent represents a merge event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#merge-request-events +type MergeEvent struct { + ObjectKind string `json:"object_kind"` + User *User `json:"user"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + VisibilityLevel VisibilityLevelValue `json:"visibility_level"` + } `json:"project"` + ObjectAttributes struct { + ID int `json:"id"` + TargetBranch string `json:"target_branch"` + SourceBranch string `json:"source_branch"` + SourceProjectID int `json:"source_project_id"` + AuthorID int `json:"author_id"` + AssigneeID int `json:"assignee_id"` + Title string `json:"title"` + CreatedAt string `json:"created_at"` // Should be *time.Time (see Gitlab issue #21468) + UpdatedAt string `json:"updated_at"` // Should be *time.Time (see Gitlab issue #21468) + StCommits []*Commit `json:"st_commits"` + StDiffs []*Diff `json:"st_diffs"` + MilestoneID int `json:"milestone_id"` + State string `json:"state"` + MergeStatus string `json:"merge_status"` + TargetProjectID int `json:"target_project_id"` + Iid int `json:"iid"` + Description string `json:"description"` + Position int `json:"position"` + LockedAt string `json:"locked_at"` + UpdatedByID int `json:"updated_by_id"` + MergeError string `json:"merge_error"` + MergeParams struct { + ForceRemoveSourceBranch string `json:"force_remove_source_branch"` + } `json:"merge_params"` + MergeWhenBuildSucceeds bool `json:"merge_when_build_succeeds"` + MergeUserID int `json:"merge_user_id"` + MergeCommitSha string `json:"merge_commit_sha"` + DeletedAt string `json:"deleted_at"` + ApprovalsBeforeMerge string `json:"approvals_before_merge"` + RebaseCommitSha string `json:"rebase_commit_sha"` + InProgressMergeCommitSha string `json:"in_progress_merge_commit_sha"` + LockVersion int `json:"lock_version"` + TimeEstimate int `json:"time_estimate"` + Source *Repository `json:"source"` + Target *Repository `json:"target"` + LastCommit struct { + ID string `json:"id"` + Message string `json:"message"` + Timestamp *time.Time `json:"timestamp"` + URL string `json:"url"` + Author *Author `json:"author"` + } `json:"last_commit"` + WorkInProgress bool `json:"work_in_progress"` + URL string `json:"url"` + Action string `json:"action"` + Assignee struct { + Name string `json:"name"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + } `json:"assignee"` + } `json:"object_attributes"` + Repository *Repository `json:"repository"` + Assignee struct { + Name string `json:"name"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + } `json:"assignee"` +} + +// WikiPageEvent represents a wiki page event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#wiki-page-events +type WikiPageEvent struct { + ObjectKind string `json:"object_kind"` + User *User `json:"user"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + VisibilityLevel VisibilityLevelValue `json:"visibility_level"` + } `json:"project"` + Wiki struct { + WebURL string `json:"web_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + } `json:"wiki"` + ObjectAttributes struct { + Title string `json:"title"` + Content string `json:"content"` + Format string `json:"format"` + Message string `json:"message"` + Slug string `json:"slug"` + URL string `json:"url"` + Action string `json:"action"` + } `json:"object_attributes"` +} + +// PipelineEvent represents a pipeline event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#pipeline-events +type PipelineEvent struct { + ObjectKind string `json:"object_kind"` + ObjectAttributes struct { + ID int `json:"id"` + Ref string `json:"ref"` + Tag bool `json:"tag"` + Sha string `json:"sha"` + BeforeSha string `json:"before_sha"` + Status string `json:"status"` + Stages []string `json:"stages"` + CreatedAt string `json:"created_at"` + FinishedAt string `json:"finished_at"` + Duration int `json:"duration"` + } `json:"object_attributes"` + User struct { + Name string `json:"name"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + } `json:"user"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + VisibilityLevel VisibilityLevelValue `json:"visibility_level"` + } `json:"project"` + Commit struct { + ID string `json:"id"` + Message string `json:"message"` + Timestamp time.Time `json:"timestamp"` + URL string `json:"url"` + Author struct { + Name string `json:"name"` + Email string `json:"email"` + } `json:"author"` + } `json:"commit"` + Builds []struct { + ID int `json:"id"` + Stage string `json:"stage"` + Name string `json:"name"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + StartedAt string `json:"started_at"` + FinishedAt string `json:"finished_at"` + When string `json:"when"` + Manual bool `json:"manual"` + User struct { + Name string `json:"name"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + } `json:"user"` + Runner string `json:"runner"` + ArtifactsFile struct { + Filename string `json:"filename"` + Size string `json:"size"` + } `json:"artifacts_file"` + } `json:"builds"` +} + +//BuildEvent represents a build event +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#build-events +type BuildEvent struct { + ObjectKind string `json:"object_kind"` + Ref string `json:"ref"` + Tag bool `json:"tag"` + BeforeSha string `json:"before_sha"` + Sha string `json:"sha"` + BuildID int `json:"build_id"` + BuildName string `json:"build_name"` + BuildStage string `json:"build_stage"` + BuildStatus string `json:"build_status"` + BuildStartedAt string `json:"build_started_at"` + BuildFinishedAt string `json:"build_finished_at"` + BuildDuration string `json:"build_duration"` + BuildAllowFailure bool `json:"build_allow_failure"` + ProjectID int `json:"project_id"` + ProjectName string `json:"project_name"` + User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + } `json:"user"` + Commit struct { + ID int `json:"id"` + Sha string `json:"sha"` + Message string `json:"message"` + AuthorName string `json:"author_name"` + AuthorEmail string `json:"author_email"` + Status string `json:"status"` + Duration string `json:"duration"` + StartedAt string `json:"started_at"` + FinishedAt string `json:"finished_at"` + } `json:"commit"` + Repository *Repository `json:"repository"` +} diff --git a/vendor/github.com/xanzy/go-gitlab/gitlab.go b/vendor/github.com/xanzy/go-gitlab/gitlab.go new file mode 100644 index 000000000..655485d3c --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/gitlab.go @@ -0,0 +1,605 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + + "github.com/google/go-querystring/query" +) + +const ( + libraryVersion = "0.1.1" + defaultBaseURL = "https://gitlab.com/api/v3/" + userAgent = "go-gitlab/" + libraryVersion +) + +// tokenType represents a token type within GitLab. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/ +type tokenType int + +// List of available token type +// +// GitLab API docs: https://docs.gitlab.com/ce/api/ +const ( + privateToken tokenType = iota + oAuthToken +) + +// AccessLevelValue represents a permission level within GitLab. +// +// GitLab API docs: https://docs.gitlab.com/ce/permissions/permissions.html +type AccessLevelValue int + +// List of available access levels +// +// GitLab API docs: https://docs.gitlab.com/ce/permissions/permissions.html +const ( + GuestPermissions AccessLevelValue = 10 + ReporterPermissions AccessLevelValue = 20 + DeveloperPermissions AccessLevelValue = 30 + MasterPermissions AccessLevelValue = 40 + OwnerPermission AccessLevelValue = 50 +) + +// NotificationLevelValue represents a notification level. +type NotificationLevelValue int + +// String implements the fmt.Stringer interface. +func (l NotificationLevelValue) String() string { + return notificationLevelNames[l] +} + +// MarshalJSON implements the json.Marshaler interface. +func (l NotificationLevelValue) MarshalJSON() ([]byte, error) { + return json.Marshal(l.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (l *NotificationLevelValue) UnmarshalJSON(data []byte) error { + var raw interface{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + switch raw := raw.(type) { + case float64: + *l = NotificationLevelValue(raw) + case string: + *l = notificationLevelTypes[raw] + default: + return fmt.Errorf("json: cannot unmarshal %T into Go value of type %T", raw, *l) + } + + return nil +} + +// List of valid notification levels. +const ( + DisabledNotificationLevel NotificationLevelValue = iota + ParticipatingNotificationLevel + WatchNotificationLevel + GlobalNotificationLevel + MentionNotificationLevel + CustomNotificationLevel +) + +var notificationLevelNames = [...]string{ + "disabled", + "participating", + "watch", + "global", + "mention", + "custom", +} + +var notificationLevelTypes = map[string]NotificationLevelValue{ + "disabled": DisabledNotificationLevel, + "participating": ParticipatingNotificationLevel, + "watch": WatchNotificationLevel, + "global": GlobalNotificationLevel, + "mention": MentionNotificationLevel, + "custom": CustomNotificationLevel, +} + +// VisibilityLevelValue represents a visibility level within GitLab. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/ +type VisibilityLevelValue int + +// List of available visibility levels +// +// GitLab API docs: https://docs.gitlab.com/ce/api/ +const ( + PrivateVisibility VisibilityLevelValue = 0 + InternalVisibility VisibilityLevelValue = 10 + PublicVisibility VisibilityLevelValue = 20 +) + +// A Client manages communication with the GitLab API. +type Client struct { + // HTTP client used to communicate with the API. + client *http.Client + + // Base URL for API requests. Defaults to the public GitLab API, but can be + // set to a domain endpoint to use with aself hosted GitLab server. baseURL + // should always be specified with a trailing slash. + baseURL *url.URL + + // token type used to make authenticated API calls. + tokenType tokenType + + // token used to make authenticated API calls. + token string + + // User agent used when communicating with the GitLab API. + UserAgent string + + // Services used for talking to different parts of the GitLab API. + Branches *BranchesService + BuildVariables *BuildVariablesService + Builds *BuildsService + Commits *CommitsService + DeployKeys *DeployKeysService + Groups *GroupsService + Issues *IssuesService + Labels *LabelsService + MergeRequests *MergeRequestsService + Milestones *MilestonesService + Namespaces *NamespacesService + Notes *NotesService + NotificationSettings *NotificationSettingsService + Projects *ProjectsService + ProjectSnippets *ProjectSnippetsService + Pipelines *PipelinesService + Repositories *RepositoriesService + RepositoryFiles *RepositoryFilesService + Services *ServicesService + Session *SessionService + Settings *SettingsService + SystemHooks *SystemHooksService + Tags *TagsService + TimeStats *TimeStatsService + Users *UsersService +} + +// ListOptions specifies the optional parameters to various List methods that +// support pagination. +type ListOptions struct { + // For paginated result sets, page of results to retrieve. + Page int `url:"page,omitempty" json:"page,omitempty"` + + // For paginated result sets, the number of results to include per page. + PerPage int `url:"per_page,omitempty" json:"per_page,omitempty"` +} + +// NewClient returns a new GitLab API client. If a nil httpClient is +// provided, http.DefaultClient will be used. To use API methods which require +// authentication, provide a valid private token. +func NewClient(httpClient *http.Client, token string) *Client { + return newClient(httpClient, privateToken, token) +} + +// NewOAuthClient returns a new GitLab API client. If a nil httpClient is +// provided, http.DefaultClient will be used. To use API methods which require +// authentication, provide a valid oauth token. +func NewOAuthClient(httpClient *http.Client, token string) *Client { + return newClient(httpClient, oAuthToken, token) +} + +func newClient(httpClient *http.Client, tokenType tokenType, token string) *Client { + if httpClient == nil { + httpClient = http.DefaultClient + } + + c := &Client{client: httpClient, tokenType: tokenType, token: token, UserAgent: userAgent} + if err := c.SetBaseURL(defaultBaseURL); err != nil { + // should never happen since defaultBaseURL is our constant + panic(err) + } + + c.Branches = &BranchesService{client: c} + c.BuildVariables = &BuildVariablesService{client: c} + c.Builds = &BuildsService{client: c} + c.Commits = &CommitsService{client: c} + c.DeployKeys = &DeployKeysService{client: c} + c.Groups = &GroupsService{client: c} + c.Issues = &IssuesService{client: c} + c.Labels = &LabelsService{client: c} + c.MergeRequests = &MergeRequestsService{client: c} + c.Milestones = &MilestonesService{client: c} + c.Namespaces = &NamespacesService{client: c} + c.Notes = &NotesService{client: c} + c.NotificationSettings = &NotificationSettingsService{client: c} + c.Projects = &ProjectsService{client: c} + c.ProjectSnippets = &ProjectSnippetsService{client: c} + c.Pipelines = &PipelinesService{client: c} + c.Repositories = &RepositoriesService{client: c} + c.RepositoryFiles = &RepositoryFilesService{client: c} + c.Services = &ServicesService{client: c} + c.Session = &SessionService{client: c} + c.Settings = &SettingsService{client: c} + c.SystemHooks = &SystemHooksService{client: c} + c.Tags = &TagsService{client: c} + c.TimeStats = &TimeStatsService{client: c} + c.Users = &UsersService{client: c} + + return c +} + +// BaseURL return a copy of the baseURL. +func (c *Client) BaseURL() *url.URL { + u := *c.baseURL + return &u +} + +// SetBaseURL sets the base URL for API requests to a custom endpoint. urlStr +// should always be specified with a trailing slash. +func (c *Client) SetBaseURL(urlStr string) error { + // Make sure the given URL end with a slash + if !strings.HasSuffix(urlStr, "/") { + urlStr += "/" + } + + var err error + c.baseURL, err = url.Parse(urlStr) + return err +} + +// NewRequest creates an API request. A relative URL path can be provided in +// urlStr, in which case it is resolved relative to the base URL of the Client. +// Relative URL paths should always be specified without a preceding slash. If +// specified, the value pointed to by body is JSON encoded and included as the +// request body. +func (c *Client) NewRequest(method, path string, opt interface{}, options []OptionFunc) (*http.Request, error) { + u := *c.baseURL + // Set the encoded opaque data + u.Opaque = c.baseURL.Path + path + + if opt != nil { + q, err := query.Values(opt) + if err != nil { + return nil, err + } + u.RawQuery = q.Encode() + } + + req := &http.Request{ + Method: method, + URL: &u, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Host: u.Host, + } + + for _, fn := range options { + if err := fn(req); err != nil { + return nil, err + } + } + + if method == "POST" || method == "PUT" { + bodyBytes, err := json.Marshal(opt) + if err != nil { + return nil, err + } + bodyReader := bytes.NewReader(bodyBytes) + + u.RawQuery = "" + req.Body = ioutil.NopCloser(bodyReader) + req.ContentLength = int64(bodyReader.Len()) + req.Header.Set("Content-Type", "application/json") + } + + req.Header.Set("Accept", "application/json") + + switch c.tokenType { + case privateToken: + req.Header.Set("PRIVATE-TOKEN", c.token) + case oAuthToken: + req.Header.Set("Authorization", "Bearer "+c.token) + } + + if c.UserAgent != "" { + req.Header.Set("User-Agent", c.UserAgent) + } + + return req, nil +} + +// Response is a GitLab API response. This wraps the standard http.Response +// returned from GitLab and provides convenient access to things like +// pagination links. +type Response struct { + *http.Response + + // These fields provide the page values for paginating through a set of + // results. Any or all of these may be set to the zero value for + // responses that are not part of a paginated set, or for which there + // are no additional pages. + + NextPage int + PrevPage int + FirstPage int + LastPage int +} + +// newResponse creats a new Response for the provided http.Response. +func newResponse(r *http.Response) *Response { + response := &Response{Response: r} + response.populatePageValues() + return response +} + +// populatePageValues parses the HTTP Link response headers and populates the +// various pagination link values in the Reponse. +func (r *Response) populatePageValues() { + if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 { + for _, link := range strings.Split(links[0], ",") { + segments := strings.Split(strings.TrimSpace(link), ";") + + // link must at least have href and rel + if len(segments) < 2 { + continue + } + + // ensure href is properly formatted + if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") { + continue + } + + // try to pull out page parameter + url, err := url.Parse(segments[0][1 : len(segments[0])-1]) + if err != nil { + continue + } + page := url.Query().Get("page") + if page == "" { + continue + } + + for _, segment := range segments[1:] { + switch strings.TrimSpace(segment) { + case `rel="next"`: + r.NextPage, _ = strconv.Atoi(page) + case `rel="prev"`: + r.PrevPage, _ = strconv.Atoi(page) + case `rel="first"`: + r.FirstPage, _ = strconv.Atoi(page) + case `rel="last"`: + r.LastPage, _ = strconv.Atoi(page) + } + + } + } + } +} + +// Do sends an API request and returns the API response. The API response is +// JSON decoded and stored in the value pointed to by v, or returned as an +// error if an API error has occurred. If v implements the io.Writer +// interface, the raw response body will be written to v, without attempting to +// first decode it. +func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + response := newResponse(resp) + + err = CheckResponse(resp) + if err != nil { + // even though there was an error, we still return the response + // in case the caller wants to inspect it further + return response, err + } + + if v != nil { + if w, ok := v.(io.Writer); ok { + _, err = io.Copy(w, resp.Body) + } else { + err = json.NewDecoder(resp.Body).Decode(v) + } + } + return response, err +} + +// Helper function to accept and format both the project ID or name as project +// identifier for all API calls. +func parseID(id interface{}) (string, error) { + switch v := id.(type) { + case int: + return strconv.Itoa(v), nil + case string: + return v, nil + default: + return "", fmt.Errorf("invalid ID type %#v, the ID must be an int or a string", id) + } +} + +// An ErrorResponse reports one or more errors caused by an API request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/README.html#data-validation-and-error-reporting +type ErrorResponse struct { + Response *http.Response + Message string +} + +func (e *ErrorResponse) Error() string { + path, _ := url.QueryUnescape(e.Response.Request.URL.Opaque) + u := fmt.Sprintf("%s://%s%s", e.Response.Request.URL.Scheme, e.Response.Request.URL.Host, path) + return fmt.Sprintf("%s %s: %d %s", e.Response.Request.Method, u, e.Response.StatusCode, e.Message) +} + +// CheckResponse checks the API response for errors, and returns them if present. +func CheckResponse(r *http.Response) error { + switch r.StatusCode { + case 200, 201, 304: + return nil + } + + errorResponse := &ErrorResponse{Response: r} + data, err := ioutil.ReadAll(r.Body) + if err == nil && data != nil { + var raw interface{} + if err := json.Unmarshal(data, &raw); err != nil { + errorResponse.Message = "failed to parse unknown error format" + } + + errorResponse.Message = parseError(raw) + } + + return errorResponse +} + +// Format: +// { +// "message": { +// "": [ +// "", +// "", +// ... +// ], +// "": { +// "": [ +// "", +// "", +// ... +// ], +// } +// }, +// "error": "" +// } +func parseError(raw interface{}) string { + switch raw := raw.(type) { + case string: + return raw + + case []interface{}: + var errs []string + for _, v := range raw { + errs = append(errs, parseError(v)) + } + return fmt.Sprintf("[%s]", strings.Join(errs, ", ")) + + case map[string]interface{}: + var errs []string + for k, v := range raw { + errs = append(errs, fmt.Sprintf("{%s: %s}", k, parseError(v))) + } + sort.Strings(errs) + return fmt.Sprintf("%s", strings.Join(errs, ", ")) + + default: + return fmt.Sprintf("failed to parse unexpected error type: %T", raw) + } +} + +// OptionFunc can be passed to all API requests to make the API call as if you were +// another user, provided your private token is from an administrator account. +// +// GitLab docs: https://docs.gitlab.com/ce/api/README.html#sudo +type OptionFunc func(*http.Request) error + +// WithSudo takes either a username or user ID and sets the SUDO request header +func WithSudo(uid interface{}) OptionFunc { + return func(req *http.Request) error { + switch uid := uid.(type) { + case int: + req.Header.Set("SUDO", strconv.Itoa(uid)) + return nil + case string: + req.Header.Set("SUDO", uid) + return nil + default: + return fmt.Errorf("uid must be either a username or user ID") + } + } +} + +// WithContext runs the request with the provided context +func WithContext(ctx context.Context) OptionFunc { + return func(req *http.Request) error { + *req = *req.WithContext(ctx) + return nil + } +} + +// Bool is a helper routine that allocates a new bool value +// to store v and returns a pointer to it. +func Bool(v bool) *bool { + p := new(bool) + *p = v + return p +} + +// Int is a helper routine that allocates a new int32 value +// to store v and returns a pointer to it, but unlike Int32 +// its argument value is an int. +func Int(v int) *int { + p := new(int) + *p = v + return p +} + +// String is a helper routine that allocates a new string value +// to store v and returns a pointer to it. +func String(v string) *string { + p := new(string) + *p = v + return p +} + +// AccessLevel is a helper routine that allocates a new AccessLevelValue +// to store v and returns a pointer to it. +func AccessLevel(v AccessLevelValue) *AccessLevelValue { + p := new(AccessLevelValue) + *p = v + return p +} + +// NotificationLevel is a helper routine that allocates a new NotificationLevelValue +// to store v and returns a pointer to it. +func NotificationLevel(v NotificationLevelValue) *NotificationLevelValue { + p := new(NotificationLevelValue) + *p = v + return p +} + +// VisibilityLevel is a helper routine that allocates a new VisibilityLevelValue +// to store v and returns a pointer to it. +func VisibilityLevel(v VisibilityLevelValue) *VisibilityLevelValue { + p := new(VisibilityLevelValue) + *p = v + return p +} diff --git a/vendor/github.com/xanzy/go-gitlab/groups.go b/vendor/github.com/xanzy/go-gitlab/groups.go new file mode 100644 index 000000000..6e5e00187 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/groups.go @@ -0,0 +1,358 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "time" +) + +// GroupsService handles communication with the group related methods of +// the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html +type GroupsService struct { + client *Client +} + +// Group represents a GitLab group. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html +type Group struct { + ID int `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Description string `json:"description"` + Projects []*Project `json:"projects"` + Statistics *StorageStatistics `json:"statistics"` +} + +// ListGroupsOptions represents the available ListGroups() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#list-project-groups +type ListGroupsOptions struct { + ListOptions + Search *string `url:"search,omitempty" json:"search,omitempty"` + Statistics *bool `url:"statistics,omitempty" json:"statistics,omitempty"` +} + +// ListGroups gets a list of groups. (As user: my groups, as admin: all groups) +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/groups.html#list-project-groups +func (s *GroupsService) ListGroups(opt *ListGroupsOptions, options ...OptionFunc) ([]*Group, *Response, error) { + req, err := s.client.NewRequest("GET", "groups", opt, options) + if err != nil { + return nil, nil, err + } + + var g []*Group + resp, err := s.client.Do(req, &g) + if err != nil { + return nil, resp, err + } + + return g, resp, err +} + +// GetGroup gets all details of a group. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#details-of-a-group +func (s *GroupsService) GetGroup(gid interface{}, options ...OptionFunc) (*Group, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s", group) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + g := new(Group) + resp, err := s.client.Do(req, g) + if err != nil { + return nil, resp, err + } + + return g, resp, err +} + +// CreateGroupOptions represents the available CreateGroup() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#new-group +type CreateGroupOptions struct { + Name *string `url:"name,omitempty" json:"name,omitempty"` + Path *string `url:"path,omitempty" json:"path,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + VisibilityLevel *VisibilityLevelValue `url:"visibility_level" json:"visibility_level,omitempty"` +} + +// CreateGroup creates a new project group. Available only for users who can +// create groups. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#new-group +func (s *GroupsService) CreateGroup(opt *CreateGroupOptions, options ...OptionFunc) (*Group, *Response, error) { + req, err := s.client.NewRequest("POST", "groups", opt, options) + if err != nil { + return nil, nil, err + } + + g := new(Group) + resp, err := s.client.Do(req, g) + if err != nil { + return nil, resp, err + } + + return g, resp, err +} + +// TransferGroup transfers a project to the Group namespace. Available only +// for admin. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/groups.html#transfer-project-to-group +func (s *GroupsService) TransferGroup(gid interface{}, project int, options ...OptionFunc) (*Group, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/projects/%d", group, project) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + g := new(Group) + resp, err := s.client.Do(req, g) + if err != nil { + return nil, resp, err + } + + return g, resp, err +} + +// DeleteGroup removes group with all projects inside. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#remove-group +func (s *GroupsService) DeleteGroup(gid interface{}, options ...OptionFunc) (*Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("groups/%s", group) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// SearchGroup get all groups that match your string in their name or path. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#search-for-group +func (s *GroupsService) SearchGroup(query string, options ...OptionFunc) ([]*Group, *Response, error) { + var q struct { + Search string `url:"search,omitempty" json:"search,omitempty"` + } + q.Search = query + + req, err := s.client.NewRequest("GET", "groups", &q, options) + if err != nil { + return nil, nil, err + } + + var g []*Group + resp, err := s.client.Do(req, &g) + if err != nil { + return nil, resp, err + } + + return g, resp, err +} + +// GroupMember represents a GitLab group member. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html +type GroupMember struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + AccessLevel AccessLevelValue `json:"access_level"` +} + +// ListGroupMembersOptions represents the available ListGroupMembers() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/groups.html#list-group-members +type ListGroupMembersOptions struct { + ListOptions +} + +// ListGroupMembers get a list of group members viewable by the authenticated +// user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/groups.html#list-group-members +func (s *GroupsService) ListGroupMembers(gid interface{}, opt *ListGroupMembersOptions, options ...OptionFunc) ([]*GroupMember, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/members", group) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var g []*GroupMember + resp, err := s.client.Do(req, &g) + if err != nil { + return nil, resp, err + } + + return g, resp, err +} + +// ListGroupProjectsOptions represents the available ListGroupProjects() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/groups.html#list-a-group-s-projects +type ListGroupProjectsOptions struct { + ListOptions +} + +// ListGroupProjects get a list of group projects +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/groups.html#list-a-group-s-projects +func (s *GroupsService) ListGroupProjects(gid interface{}, opt *ListGroupProjectsOptions, options ...OptionFunc) ([]*Project, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/projects", group) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var p []*Project + resp, err := s.client.Do(req, &p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// AddGroupMemberOptions represents the available AddGroupMember() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#add-group-member +type AddGroupMemberOptions struct { + UserID *int `url:"user_id,omitempty" json:"user_id,omitempty"` + AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"` +} + +// AddGroupMember adds a user to the list of group members. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/groups.html#list-group-members +func (s *GroupsService) AddGroupMember(gid interface{}, opt *AddGroupMemberOptions, options ...OptionFunc) (*GroupMember, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/members", group) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + g := new(GroupMember) + resp, err := s.client.Do(req, g) + if err != nil { + return nil, resp, err + } + + return g, resp, err +} + +// UpdateGroupMemberOptions represents the available UpdateGroupMember() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/groups.html#edit-group-team-member +type UpdateGroupMemberOptions struct { + AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"` +} + +// UpdateGroupMember updates a group team member to a specified access level. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/groups.html#list-group-members +func (s *GroupsService) UpdateGroupMember(gid interface{}, user int, opt *UpdateGroupMemberOptions, options ...OptionFunc) (*GroupMember, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/members/%d", group, user) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + g := new(GroupMember) + resp, err := s.client.Do(req, g) + if err != nil { + return nil, resp, err + } + + return g, resp, err +} + +// RemoveGroupMember removes user from user team. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/groups.html#remove-user-from-user-team +func (s *GroupsService) RemoveGroupMember(gid interface{}, user int, options ...OptionFunc) (*Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("groups/%s/members/%d", group, user) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/vendor/github.com/xanzy/go-gitlab/issues.go b/vendor/github.com/xanzy/go-gitlab/issues.go new file mode 100644 index 000000000..0a286c203 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/issues.go @@ -0,0 +1,264 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + "time" +) + +// IssuesService handles communication with the issue related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html +type IssuesService struct { + client *Client +} + +// Issue represents a GitLab issue. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html +type Issue struct { + ID int `json:"id"` + IID int `json:"iid"` + ProjectID int `json:"project_id"` + Title string `json:"title"` + Description string `json:"description"` + Labels []string `json:"labels"` + Milestone *Milestone `json:"milestone"` + Assignee struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + } `json:"assignee"` + Author struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + } `json:"author"` + State string `json:"state"` + UpdatedAt *time.Time `json:"updated_at"` + CreatedAt *time.Time `json:"created_at"` + Subscribed bool `json:"subscribed"` + UserNotesCount int `json:"user_notes_count"` + Confidential bool `json:"confidential"` + DueDate string `json:"due_date"` + WebURL string `json:"web_url"` +} + +func (i Issue) String() string { + return Stringify(i) +} + +// Labels is a custom type with specific marshaling characteristics. +type Labels []string + +// MarshalJSON implements the json.Marshaler interface. +func (l *Labels) MarshalJSON() ([]byte, error) { + return json.Marshal(strings.Join(*l, ",")) +} + +// ListIssuesOptions represents the available ListIssues() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-issues +type ListIssuesOptions struct { + ListOptions + State *string `url:"state,omitempty" json:"state,omitempty"` + Labels Labels `url:"labels,comma,omitempty" json:"labels,omitempty"` + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` +} + +// ListIssues gets all issues created by authenticated user. This function +// takes pagination parameters page and per_page to restrict the list of issues. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-issues +func (s *IssuesService) ListIssues(opt *ListIssuesOptions, options ...OptionFunc) ([]*Issue, *Response, error) { + req, err := s.client.NewRequest("GET", "issues", opt, options) + if err != nil { + return nil, nil, err + } + + var i []*Issue + resp, err := s.client.Do(req, &i) + if err != nil { + return nil, resp, err + } + + return i, resp, err +} + +// ListProjectIssuesOptions represents the available ListProjectIssues() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-issues +type ListProjectIssuesOptions struct { + ListOptions + IID *int `url:"iid,omitempty" json:"iid,omitempty"` + State *string `url:"state,omitempty" json:"state,omitempty"` + Labels Labels `url:"labels,comma,omitempty" json:"labels,omitempty"` + Milestone *string `url:"milestone,omitempty" json:"milestone,omitempty"` + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` +} + +// ListProjectIssues gets a list of project issues. This function accepts +// pagination parameters page and per_page to return the list of project issues. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-project-issues +func (s *IssuesService) ListProjectIssues(pid interface{}, opt *ListProjectIssuesOptions, options ...OptionFunc) ([]*Issue, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var i []*Issue + resp, err := s.client.Do(req, &i) + if err != nil { + return nil, resp, err + } + + return i, resp, err +} + +// GetIssue gets a single project issue. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#single-issues +func (s *IssuesService) GetIssue(pid interface{}, issue int, options ...OptionFunc) (*Issue, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + i := new(Issue) + resp, err := s.client.Do(req, i) + if err != nil { + return nil, resp, err + } + + return i, resp, err +} + +// CreateIssueOptions represents the available CreateIssue() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#new-issues +type CreateIssueOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` + MilestoneID *int `url:"milestone_id,omitempty" json:"milestone_id,omitempty"` + Labels Labels `url:"labels,comma,omitempty" json:"labels,omitempty"` +} + +// CreateIssue creates a new project issue. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#new-issues +func (s *IssuesService) CreateIssue(pid interface{}, opt *CreateIssueOptions, options ...OptionFunc) (*Issue, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + i := new(Issue) + resp, err := s.client.Do(req, i) + if err != nil { + return nil, resp, err + } + + return i, resp, err +} + +// UpdateIssueOptions represents the available UpdateIssue() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#edit-issues +type UpdateIssueOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` + MilestoneID *int `url:"milestone_id,omitempty" json:"milestone_id,omitempty"` + Labels Labels `url:"labels,comma,omitempty" json:"labels,omitempty"` + StateEvent *string `url:"state_event,omitempty" json:"state_event,omitempty"` +} + +// UpdateIssue updates an existing project issue. This function is also used +// to mark an issue as closed. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#edit-issues +func (s *IssuesService) UpdateIssue(pid interface{}, issue int, opt *UpdateIssueOptions, options ...OptionFunc) (*Issue, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + i := new(Issue) + resp, err := s.client.Do(req, i) + if err != nil { + return nil, resp, err + } + + return i, resp, err +} + +// DeleteIssue deletes a single project issue. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#delete-an-issue +func (s *IssuesService) DeleteIssue(pid interface{}, issue int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/vendor/github.com/xanzy/go-gitlab/labels.go b/vendor/github.com/xanzy/go-gitlab/labels.go new file mode 100644 index 000000000..8d571b7e8 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/labels.go @@ -0,0 +1,164 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// LabelsService handles communication with the label related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html +type LabelsService struct { + client *Client +} + +// Label represents a GitLab label. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html +type Label struct { + Name string `json:"name"` + Color string `json:"color"` + Description string `json:"description"` + OpenIssuesCount int `json:"open_issues_count"` + ClosedIssuesCount int `json:"closed_issues_count"` + OpenMergeRequestsCount int `json:"open_merge_requests_count"` +} + +func (l Label) String() string { + return Stringify(l) +} + +// ListLabels gets all labels for given project. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#list-labels +func (s *LabelsService) ListLabels(pid interface{}, options ...OptionFunc) ([]*Label, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/labels", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var l []*Label + resp, err := s.client.Do(req, &l) + if err != nil { + return nil, resp, err + } + + return l, resp, err +} + +// CreateLabelOptions represents the available CreateLabel() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#create-a-new-label +type CreateLabelOptions struct { + Name *string `url:"name,omitempty" json:"name,omitempty"` + Color *string `url:"color,omitempty" json:"color,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` +} + +// CreateLabel creates a new label for given repository with given name and +// color. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#create-a-new-label +func (s *LabelsService) CreateLabel(pid interface{}, opt *CreateLabelOptions, options ...OptionFunc) (*Label, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/labels", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + l := new(Label) + resp, err := s.client.Do(req, l) + if err != nil { + return nil, resp, err + } + + return l, resp, err +} + +// DeleteLabelOptions represents the available DeleteLabel() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#delete-a-label +type DeleteLabelOptions struct { + Name *string `url:"name,omitempty" json:"name,omitempty"` +} + +// DeleteLabel deletes a label given by its name. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#delete-a-label +func (s *LabelsService) DeleteLabel(pid interface{}, opt *DeleteLabelOptions, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/labels", url.QueryEscape(project)) + + req, err := s.client.NewRequest("DELETE", u, opt, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// UpdateLabelOptions represents the available UpdateLabel() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#delete-a-label +type UpdateLabelOptions struct { + Name *string `url:"name,omitempty" json:"name,omitempty"` + NewName *string `url:"new_name,omitempty" json:"new_name,omitempty"` + Color *string `url:"color,omitempty" json:"color,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` +} + +// UpdateLabel updates an existing label with new name or now color. At least +// one parameter is required, to update the label. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#edit-an-existing-label +func (s *LabelsService) UpdateLabel(pid interface{}, opt *UpdateLabelOptions, options ...OptionFunc) (*Label, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/labels", url.QueryEscape(project)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + l := new(Label) + resp, err := s.client.Do(req, l) + if err != nil { + return nil, resp, err + } + + return l, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/merge_requests.go b/vendor/github.com/xanzy/go-gitlab/merge_requests.go new file mode 100644 index 000000000..1d154067b --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/merge_requests.go @@ -0,0 +1,333 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// MergeRequestsService handles communication with the merge requests related +// methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/merge_requests.html +type MergeRequestsService struct { + client *Client +} + +// MergeRequest represents a GitLab merge request. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/merge_requests.html +type MergeRequest struct { + ID int `json:"id"` + IID int `json:"iid"` + ProjectID int `json:"project_id"` + Title string `json:"title"` + Description string `json:"description"` + WorkInProgress bool `json:"work_in_progress"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + TargetBranch string `json:"target_branch"` + SourceBranch string `json:"source_branch"` + Upvotes int `json:"upvotes"` + Downvotes int `json:"downvotes"` + Author struct { + Name string `json:"name"` + Username string `json:"username"` + ID int `json:"id"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + } `json:"author"` + Assignee struct { + Name string `json:"name"` + Username string `json:"username"` + ID int `json:"id"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + } `json:"assignee"` + SourceProjectID int `json:"source_project_id"` + TargetProjectID int `json:"target_project_id"` + Labels []string `json:"labels"` + Milestone struct { + ID int `json:"id"` + Iid int `json:"iid"` + ProjectID int `json:"project_id"` + Title string `json:"title"` + Description string `json:"description"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + DueDate string `json:"due_date"` + } `json:"milestone"` + MergeWhenBuildSucceeds bool `json:"merge_when_build_succeeds"` + MergeStatus string `json:"merge_status"` + Subscribed bool `json:"subscribed"` + UserNotesCount int `json:"user_notes_count"` + SouldRemoveSourceBranch bool `json:"should_remove_source_branch"` + ForceRemoveSourceBranch bool `json:"force_remove_source_branch"` + Changes []struct { + OldPath string `json:"old_path"` + NewPath string `json:"new_path"` + AMode string `json:"a_mode"` + BMode string `json:"b_mode"` + Diff string `json:"diff"` + NewFile bool `json:"new_file"` + RenamedFile bool `json:"renamed_file"` + DeletedFile bool `json:"deleted_file"` + } `json:"changes"` + WebURL string `json:"web_url"` +} + +func (m MergeRequest) String() string { + return Stringify(m) +} + +// ListMergeRequestsOptions represents the available ListMergeRequests() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#list-merge-requests +type ListMergeRequestsOptions struct { + ListOptions + IID *int `url:"iid,omitempty" json:"iid,omitempty"` + State *string `url:"state,omitempty" json:"state,omitempty"` + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` +} + +// ListMergeRequests gets all merge requests for this project. The state +// parameter can be used to get only merge requests with a given state (opened, +// closed, or merged) or all of them (all). The pagination parameters page and +// per_page can be used to restrict the list of merge requests. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#list-merge-requests +func (s *MergeRequestsService) ListMergeRequests(pid interface{}, opt *ListMergeRequestsOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var m []*MergeRequest + resp, err := s.client.Do(req, &m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// GetMergeRequest shows information about a single merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#get-single-mr +func (s *MergeRequestsService) GetMergeRequest(pid interface{}, mergeRequest int, options ...OptionFunc) (*MergeRequest, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + m := new(MergeRequest) + resp, err := s.client.Do(req, m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// GetMergeRequestCommits gets a list of merge request commits. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#get-single-mr-commits +func (s *MergeRequestsService) GetMergeRequestCommits(pid interface{}, mergeRequest int, options ...OptionFunc) ([]*Commit, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/commits", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var c []*Commit + resp, err := s.client.Do(req, &c) + if err != nil { + return nil, resp, err + } + + return c, resp, err +} + +// GetMergeRequestChanges shows information about the merge request including +// its files and changes. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#get-single-mr-changes +func (s *MergeRequestsService) GetMergeRequestChanges(pid interface{}, mergeRequest int, options ...OptionFunc) (*MergeRequest, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/changes", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + m := new(MergeRequest) + resp, err := s.client.Do(req, m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// CreateMergeRequestOptions represents the available CreateMergeRequest() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#create-mr +type CreateMergeRequestOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + SourceBranch *string `url:"source_branch,omitemtpy" json:"source_branch,omitemtpy"` + TargetBranch *string `url:"target_branch,omitemtpy" json:"target_branch,omitemtpy"` + AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` + TargetProjectID *int `url:"target_project_id,omitempty" json:"target_project_id,omitempty"` +} + +// CreateMergeRequest creates a new merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#create-mr +func (s *MergeRequestsService) CreateMergeRequest(pid interface{}, opt *CreateMergeRequestOptions, options ...OptionFunc) (*MergeRequest, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + m := new(MergeRequest) + resp, err := s.client.Do(req, m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// UpdateMergeRequestOptions represents the available UpdateMergeRequest() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#update-mr +type UpdateMergeRequestOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + TargetBranch *string `url:"target_branch,omitemtpy" json:"target_branch,omitemtpy"` + AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` + StateEvent *string `url:"state_event,omitempty" json:"state_event,omitempty"` +} + +// UpdateMergeRequest updates an existing project milestone. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#update-mr +func (s *MergeRequestsService) UpdateMergeRequest(pid interface{}, mergeRequest int, opt *UpdateMergeRequestOptions, options ...OptionFunc) (*MergeRequest, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + m := new(MergeRequest) + resp, err := s.client.Do(req, m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// AcceptMergeRequestOptions represents the available AcceptMergeRequest() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#accept-mr +type AcceptMergeRequestOptions struct { + MergeCommitMessage *string `url:"merge_commit_message,omitempty" json:"merge_commit_message,omitempty"` + ShouldRemoveSourceBranch *bool `url:"should_remove_source_branch,omitempty" json:"should_remove_source_branch,omitempty"` + MergeWhenBuildSucceeds *bool `url:"merge_when_build_succeeds,omitempty" json:"merge_when_build_succeeds,omitempty"` + Sha *string `url:"sha,omitempty" json:"sha,omitempty"` +} + +// AcceptMergeRequest merges changes submitted with MR using this API. If merge +// success you get 200 OK. If it has some conflicts and can not be merged - you +// get 405 and error message 'Branch cannot be merged'. If merge request is +// already merged or closed - you get 405 and error message 'Method Not Allowed' +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#accept-mr +func (s *MergeRequestsService) AcceptMergeRequest(pid interface{}, mergeRequest int, opt *AcceptMergeRequestOptions, options ...OptionFunc) (*MergeRequest, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/merge", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + m := new(MergeRequest) + resp, err := s.client.Do(req, m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/milestones.go b/vendor/github.com/xanzy/go-gitlab/milestones.go new file mode 100644 index 000000000..eeaf9cce2 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/milestones.go @@ -0,0 +1,216 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// MilestonesService handles communication with the milestone related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/milestones.html +type MilestonesService struct { + client *Client +} + +// Milestone represents a GitLab milestone. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/milestones.html +type Milestone struct { + ID int `json:"id"` + Iid int `json:"iid"` + ProjectID int `json:"project_id"` + Title string `json:"title"` + Description string `json:"description"` + StartDate string `json:"start_date"` + DueDate string `json:"due_date"` + State string `json:"state"` + UpdatedAt *time.Time `json:"updated_at"` + CreatedAt *time.Time `json:"created_at"` +} + +func (m Milestone) String() string { + return Stringify(m) +} + +// ListMilestonesOptions represents the available ListMilestones() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/milestones.html#list-project-milestones +type ListMilestonesOptions struct { + ListOptions + IID *int `url:"iid,omitempty" json:"iid,omitempty"` +} + +// ListMilestones returns a list of project milestones. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/milestones.html#list-project-milestones +func (s *MilestonesService) ListMilestones(pid interface{}, opt *ListMilestonesOptions, options ...OptionFunc) ([]*Milestone, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/milestones", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var m []*Milestone + resp, err := s.client.Do(req, &m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// GetMilestone gets a single project milestone. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/milestones.html#get-single-milestone +func (s *MilestonesService) GetMilestone(pid interface{}, milestone int, options ...OptionFunc) (*Milestone, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/milestones/%d", url.QueryEscape(project), milestone) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + m := new(Milestone) + resp, err := s.client.Do(req, m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// CreateMilestoneOptions represents the available CreateMilestone() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/milestones.html#create-new-milestone +type CreateMilestoneOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + StartDate *string `url:"start_date,omitempty" json:"start_date,omitempty"` + DueDate *string `url:"due_date,omitempty" json:"due_date,omitempty"` +} + +// CreateMilestone creates a new project milestone. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/milestones.html#create-new-milestone +func (s *MilestonesService) CreateMilestone(pid interface{}, opt *CreateMilestoneOptions, options ...OptionFunc) (*Milestone, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/milestones", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + m := new(Milestone) + resp, err := s.client.Do(req, m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// UpdateMilestoneOptions represents the available UpdateMilestone() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/milestones.html#edit-milestone +type UpdateMilestoneOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + StartDate *string `url:"start_date,omitempty" json:"start_date,omitempty"` + DueDate *string `url:"due_date,omitempty" json:"due_date,omitempty"` + StateEvent *string `url:"state_event,omitempty" json:"state_event,omitempty"` +} + +// UpdateMilestone updates an existing project milestone. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/milestones.html#edit-milestone +func (s *MilestonesService) UpdateMilestone(pid interface{}, milestone int, opt *UpdateMilestoneOptions, options ...OptionFunc) (*Milestone, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/milestones/%d", url.QueryEscape(project), milestone) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + m := new(Milestone) + resp, err := s.client.Do(req, m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// GetMilestoneIssuesOptions represents the available GetMilestoneIssues() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/milestones.html#get-all-issues-assigned-to-a-single-milestone +type GetMilestoneIssuesOptions struct { + ListOptions +} + +// GetMilestoneIssues gets all issues assigned to a single project milestone. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/milestones.html#get-all-issues-assigned-to-a-single-milestone +func (s *MilestonesService) GetMilestoneIssues(pid interface{}, milestone int, opt *GetMilestoneIssuesOptions, options ...OptionFunc) ([]*Issue, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/milestones/%d/issues", url.QueryEscape(project), milestone) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var i []*Issue + resp, err := s.client.Do(req, &i) + if err != nil { + return nil, resp, err + } + + return i, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/namespaces.go b/vendor/github.com/xanzy/go-gitlab/namespaces.go new file mode 100644 index 000000000..d4b5e4508 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/namespaces.go @@ -0,0 +1,89 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +// NamespacesService handles communication with the namespace related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/namespaces.html +type NamespacesService struct { + client *Client +} + +// Namespace represents a GitLab namespace. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/namespaces.html +type Namespace struct { + ID int `json:"id"` + Path string `json:"path"` + Kind string `json:"kind"` +} + +func (n Namespace) String() string { + return Stringify(n) +} + +// ListNamespacesOptions represents the available ListNamespaces() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/namespaces.html#list-namespaces +type ListNamespacesOptions struct { + ListOptions + Search *string `url:"search,omitempty" json:"search,omitempty"` +} + +// ListNamespaces gets a list of projects accessible by the authenticated user. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/namespaces.html#list-namespaces +func (s *NamespacesService) ListNamespaces(opt *ListNamespacesOptions, options ...OptionFunc) ([]*Namespace, *Response, error) { + req, err := s.client.NewRequest("GET", "namespaces", opt, options) + if err != nil { + return nil, nil, err + } + + var n []*Namespace + resp, err := s.client.Do(req, &n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// SearchNamespace gets all namespaces that match your string in their name +// or path. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/namespaces.html#search-for-namespace +func (s *NamespacesService) SearchNamespace(query string, options ...OptionFunc) ([]*Namespace, *Response, error) { + var q struct { + Search string `url:"search,omitempty" json:"search,omitempty"` + } + q.Search = query + + req, err := s.client.NewRequest("GET", "namespaces", &q, options) + if err != nil { + return nil, nil, err + } + + var n []*Namespace + resp, err := s.client.Do(req, &n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/notes.go b/vendor/github.com/xanzy/go-gitlab/notes.go new file mode 100644 index 000000000..c1836c2b8 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/notes.go @@ -0,0 +1,418 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// NotesService handles communication with the notes related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/notes.html +type NotesService struct { + client *Client +} + +// Note represents a GitLab note. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/notes.html +type Note struct { + ID int `json:"id"` + Body string `json:"body"` + Attachment string `json:"attachment"` + Title string `json:"title"` + FileName string `json:"file_name"` + Author struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + } `json:"author"` + ExpiresAt *time.Time `json:"expires_at"` + UpdatedAt *time.Time `json:"updated_at"` + CreatedAt *time.Time `json:"created_at"` +} + +func (n Note) String() string { + return Stringify(n) +} + +// ListIssueNotesOptions represents the available ListIssueNotes() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#list-project-issue-notes +type ListIssueNotesOptions struct { + ListOptions +} + +// ListIssueNotes gets a list of all notes for a single issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#list-project-issue-notes +func (s *NotesService) ListIssueNotes(pid interface{}, issue int, opt *ListIssueNotesOptions, options ...OptionFunc) ([]*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/notes", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var n []*Note + resp, err := s.client.Do(req, &n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// GetIssueNote returns a single note for a specific project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#get-single-issue-note +func (s *NotesService) GetIssueNote(pid interface{}, issue int, note int, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/notes/%d", url.QueryEscape(project), issue, note) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// CreateIssueNoteOptions represents the available CreateIssueNote() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#create-new-issue-note +type CreateIssueNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` +} + +// CreateIssueNote creates a new note to a single project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#create-new-issue-note +func (s *NotesService) CreateIssueNote(pid interface{}, issue int, opt *CreateIssueNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/notes", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// UpdateIssueNoteOptions represents the available UpdateIssueNote() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#modify-existing-issue-note +type UpdateIssueNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` +} + +// UpdateIssueNote modifies existing note of an issue. +// +// https://docs.gitlab.com/ce/api/notes.html#modify-existing-issue-note +func (s *NotesService) UpdateIssueNote(pid interface{}, issue int, note int, opt *UpdateIssueNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/notes/%d", url.QueryEscape(project), issue, note) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// ListSnippetNotes gets a list of all notes for a single snippet. Snippet +// notes are comments users can post to a snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#list-all-snippet-notes +func (s *NotesService) ListSnippetNotes(pid interface{}, snippet int, options ...OptionFunc) ([]*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d/notes", url.QueryEscape(project), snippet) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var n []*Note + resp, err := s.client.Do(req, &n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// GetSnippetNote returns a single note for a given snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#get-single-snippet-note +func (s *NotesService) GetSnippetNote(pid interface{}, snippet int, note int, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d/notes/%d", url.QueryEscape(project), snippet, note) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// CreateSnippetNoteOptions represents the available CreateSnippetNote() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#create-new-snippet-note +type CreateSnippetNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` +} + +// CreateSnippetNote creates a new note for a single snippet. Snippet notes are +// comments users can post to a snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#create-new-snippet-note +func (s *NotesService) CreateSnippetNote(pid interface{}, snippet int, opt *CreateSnippetNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d/notes", url.QueryEscape(project), snippet) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// UpdateSnippetNoteOptions represents the available UpdateSnippetNote() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#modify-existing-snippet-note +type UpdateSnippetNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` +} + +// UpdateSnippetNote modifies existing note of a snippet. +// +// https://docs.gitlab.com/ce/api/notes.html#modify-existing-snippet-note +func (s *NotesService) UpdateSnippetNote(pid interface{}, snippet int, note int, opt *UpdateSnippetNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d/notes/%d", url.QueryEscape(project), snippet, note) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// ListMergeRequestNotes gets a list of all notes for a single merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#list-all-merge-request-notes +func (s *NotesService) ListMergeRequestNotes(pid interface{}, mergeRequest int, options ...OptionFunc) ([]*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/notes", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var n []*Note + resp, err := s.client.Do(req, &n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// GetMergeRequestNote returns a single note for a given merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#get-single-merge-request-note +func (s *NotesService) GetMergeRequestNote(pid interface{}, mergeRequest int, note int, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/notes/%d", url.QueryEscape(project), mergeRequest, note) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// CreateMergeRequestNoteOptions represents the available +// CreateMergeRequestNote() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#create-new-merge-request-note +type CreateMergeRequestNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` +} + +// CreateMergeRequestNote creates a new note for a single merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#create-new-merge-request-note +func (s *NotesService) CreateMergeRequestNote(pid interface{}, mergeRequest int, opt *CreateMergeRequestNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/notes", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// UpdateMergeRequestNoteOptions represents the available +// UpdateMergeRequestNote() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#modify-existing-merge-request-note +type UpdateMergeRequestNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` +} + +// UpdateMergeRequestNote modifies existing note of a merge request. +// +// https://docs.gitlab.com/ce/api/notes.html#modify-existing-merge-request-note +func (s *NotesService) UpdateMergeRequestNote(pid interface{}, mergeRequest int, note int, opt *UpdateMergeRequestNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf( + "projects/%s/merge_requests/%d/notes/%d", url.QueryEscape(project), mergeRequest, note) + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/notifications.go b/vendor/github.com/xanzy/go-gitlab/notifications.go new file mode 100644 index 000000000..04424c293 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/notifications.go @@ -0,0 +1,214 @@ +package gitlab + +import ( + "errors" + "fmt" + "net/url" +) + +// NotificationSettingsService handles communication with the notification settings +// related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/notification_settings.html +type NotificationSettingsService struct { + client *Client +} + +// NotificationSettings represents the Gitlab notification setting. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#notification-settings +type NotificationSettings struct { + Level NotificationLevelValue `json:"level"` + NotificationEmail string `json:"notification_email"` + Events *NotificationEvents `json:"events"` +} + +// NotificationEvents represents the avialable notification setting events. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#notification-settings +type NotificationEvents struct { + CloseIssue bool `json:"close_issue"` + CloseMergeRequest bool `json:"close_merge_request"` + FailedPipeline bool `json:"failed_pipeline"` + MergeMergeRequest bool `json:"merge_merge_request"` + NewIssue bool `json:"new_issue"` + NewMergeRequest bool `json:"new_merge_request"` + NewNote bool `json:"new_note"` + ReassignIssue bool `json:"reassign_issue"` + ReassignMergeRequest bool `json:"reassign_merge_request"` + ReopenIssue bool `json:"reopen_issue"` + ReopenMergeRequest bool `json:"reopen_merge_request"` + SuccessPipeline bool `json:"success_pipeline"` +} + +func (ns NotificationSettings) String() string { + return Stringify(ns) +} + +// GetGlobalSettings returns current notification settings and email address. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#global-notification-settings +func (s *NotificationSettingsService) GetGlobalSettings(options ...OptionFunc) (*NotificationSettings, *Response, error) { + u := "notification_settings" + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + ns := new(NotificationSettings) + resp, err := s.client.Do(req, ns) + if err != nil { + return nil, resp, err + } + + return ns, resp, err +} + +// NotificationSettingsOptions represents the available options that can be passed +// to the API when updating the notification settings. +type NotificationSettingsOptions struct { + Level *NotificationLevelValue `url:"level,omitempty" json:"level,omitempty"` + NotificationEmail *string `url:"notification_email,omitempty" json:"notification_email,omitempty"` + CloseIssue *bool `url:"close_issue,omitempty" json:"close_issue,omitempty"` + CloseMergeRequest *bool `url:"close_merge_request,omitempty" json:"close_merge_request,omitempty"` + FailedPipeline *bool `url:"failed_pipeline,omitempty" json:"failed_pipeline,omitempty"` + MergeMergeRequest *bool `url:"merge_merge_request,omitempty" json:"merge_merge_request,omitempty"` + NewIssue *bool `url:"new_issue,omitempty" json:"new_issue,omitempty"` + NewMergeRequest *bool `url:"new_merge_request,omitempty" json:"new_merge_request,omitempty"` + NewNote *bool `url:"new_note,omitempty" json:"new_note,omitempty"` + ReassignIssue *bool `url:"reassign_issue,omitempty" json:"reassign_issue,omitempty"` + ReassignMergeRequest *bool `url:"reassign_merge_request,omitempty" json:"reassign_merge_request,omitempty"` + ReopenIssue *bool `url:"reopen_issue,omitempty" json:"reopen_issue,omitempty"` + ReopenMergeRequest *bool `url:"reopen_merge_request,omitempty" json:"reopen_merge_request,omitempty"` + SuccessPipeline *bool `url:"success_pipeline,omitempty" json:"success_pipeline,omitempty"` +} + +// UpdateGlobalSettings updates current notification settings and email address. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#update-global-notification-settings +func (s *NotificationSettingsService) UpdateGlobalSettings(opt *NotificationSettingsOptions, options ...OptionFunc) (*NotificationSettings, *Response, error) { + if opt.Level != nil && *opt.Level == GlobalNotificationLevel { + return nil, nil, errors.New( + "notification level 'global' is not valid for global notification settings") + } + + u := "notification_settings" + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + ns := new(NotificationSettings) + resp, err := s.client.Do(req, ns) + if err != nil { + return nil, resp, err + } + + return ns, resp, err +} + +// GetSettingsForGroup returns current group notification settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#group-project-level-notification-settings +func (s *NotificationSettingsService) GetSettingsForGroup(gid interface{}, options ...OptionFunc) (*NotificationSettings, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/notification_settings", url.QueryEscape(group)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + ns := new(NotificationSettings) + resp, err := s.client.Do(req, ns) + if err != nil { + return nil, resp, err + } + + return ns, resp, err +} + +// GetSettingsForProject returns current project notification settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#group-project-level-notification-settings +func (s *NotificationSettingsService) GetSettingsForProject(pid interface{}, options ...OptionFunc) (*NotificationSettings, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/notification_settings", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + ns := new(NotificationSettings) + resp, err := s.client.Do(req, ns) + if err != nil { + return nil, resp, err + } + + return ns, resp, err +} + +// UpdateSettingsForGroup updates current group notification settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#update-group-project-level-notification-settings +func (s *NotificationSettingsService) UpdateSettingsForGroup(gid interface{}, opt *NotificationSettingsOptions, options ...OptionFunc) (*NotificationSettings, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/notification_settings", url.QueryEscape(group)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + ns := new(NotificationSettings) + resp, err := s.client.Do(req, ns) + if err != nil { + return nil, resp, err + } + + return ns, resp, err +} + +// UpdateSettingsForProject updates current project notification settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#update-group-project-level-notification-settings +func (s *NotificationSettingsService) UpdateSettingsForProject(pid interface{}, opt *NotificationSettingsOptions, options ...OptionFunc) (*NotificationSettings, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/notification_settings", url.QueryEscape(project)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + ns := new(NotificationSettings) + resp, err := s.client.Do(req, ns) + if err != nil { + return nil, resp, err + } + + return ns, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/pipelines.go b/vendor/github.com/xanzy/go-gitlab/pipelines.go new file mode 100644 index 000000000..4ade3fce9 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/pipelines.go @@ -0,0 +1,191 @@ +// +// Copyright 2017, Igor Varavko +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// PipelinesService handles communication with the repositories related +// methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html +type PipelinesService struct { + client *Client +} + +// Pipeline represents a GitLab pipeline. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html +type Pipeline struct { + ID int `json:"id"` + Status string `json:"status"` + Ref string `json:"ref"` + Sha string `json:"sha"` + BeforeSha string `json:"before_sha"` + Tag bool `json:"tag"` + YamlErrors string `json:"yaml_errors"` + User struct { + Name string `json:"name"` + Username string `json:"username"` + ID int `json:"id"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + } + UpdatedAt *time.Time `json:"updated_at"` + CreatedAt *time.Time `json:"created_at"` + StartedAt *time.Time `json:"started_at"` + FinishedAt *time.Time `json:"finished_at"` + CommittedAt *time.Time `json:"committed_at"` + Duration int `json:"duration"` + Coverage string `json:"coverage"` +} + +func (i Pipeline) String() string { + return Stringify(i) +} + +// ListProjectPipelines gets a list of project piplines. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html#list-project-pipelines +func (s *PipelinesService) ListProjectPipelines(pid interface{}, options ...OptionFunc) ([]*Pipeline, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipelines", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var p []*Pipeline + resp, err := s.client.Do(req, &p) + if err != nil { + return nil, resp, err + } + return p, resp, err +} + +// GetPipeline gets a single project pipeline. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html#get-a-single-pipeline +func (s *PipelinesService) GetPipeline(pid interface{}, pipeline int, options ...OptionFunc) (*Pipeline, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipelines/%d", url.QueryEscape(project), pipeline) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(Pipeline) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// CreatePipelineOptions represents the available CreatePipeline() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html#create-a-new-pipeline +type CreatePipelineOptions struct { + Ref *string `url:"ref,omitempty" json:"ref"` +} + +// CreatePipeline creates a new project pipeline. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html#create-a-new-pipeline +func (s *PipelinesService) CreatePipeline(pid interface{}, opt *CreatePipelineOptions, options ...OptionFunc) (*Pipeline, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipeline", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + p := new(Pipeline) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// RetryPipelineBuild retries failed builds in a pipeline +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipelines.html#retry-failed-builds-in-a-pipeline +func (s *PipelinesService) RetryPipelineBuild(pid interface{}, pipelineID int, options ...OptionFunc) (*Pipeline, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipelines/%d/retry", project, pipelineID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(Pipeline) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// CancelPipelineBuild cancels a pipeline builds +// +// GitLab API docs: +//https://docs.gitlab.com/ce/api/pipelines.html#cancel-a-pipelines-builds +func (s *PipelinesService) CancelPipelineBuild(pid interface{}, pipelineID int, options ...OptionFunc) (*Pipeline, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipelines/%d/cancel", project, pipelineID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(Pipeline) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/project_snippets.go b/vendor/github.com/xanzy/go-gitlab/project_snippets.go new file mode 100644 index 000000000..65dab67be --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/project_snippets.go @@ -0,0 +1,232 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "bytes" + "fmt" + "net/url" + "time" +) + +// ProjectSnippetsService handles communication with the project snippets +// related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/project_snippets.html +type ProjectSnippetsService struct { + client *Client +} + +// Snippet represents a GitLab project snippet. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/project_snippets.html +type Snippet struct { + ID int `json:"id"` + Title string `json:"title"` + FileName string `json:"file_name"` + Author struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + } `json:"author"` + ExpiresAt *time.Time `json:"expires_at"` + UpdatedAt *time.Time `json:"updated_at"` + CreatedAt *time.Time `json:"created_at"` +} + +func (s Snippet) String() string { + return Stringify(s) +} + +// ListSnippetsOptions represents the available ListSnippets() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/project_snippets.html#list-snippets +type ListSnippetsOptions struct { + ListOptions +} + +// ListSnippets gets a list of project snippets. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/project_snippets.html#list-snippets +func (s *ProjectSnippetsService) ListSnippets(pid interface{}, opt *ListSnippetsOptions, options ...OptionFunc) ([]*Snippet, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/snippets", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var ps []*Snippet + resp, err := s.client.Do(req, &ps) + if err != nil { + return nil, resp, err + } + + return ps, resp, err +} + +// GetSnippet gets a single project snippet +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/project_snippets.html#single-snippet +func (s *ProjectSnippetsService) GetSnippet(pid interface{}, snippet int, options ...OptionFunc) (*Snippet, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d", url.QueryEscape(project), snippet) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + ps := new(Snippet) + resp, err := s.client.Do(req, ps) + if err != nil { + return nil, resp, err + } + + return ps, resp, err +} + +// CreateSnippetOptions represents the available CreateSnippet() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/project_snippets.html#create-new-snippet +type CreateSnippetOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + FileName *string `url:"file_name,omitempty" json:"file_name,omitempty"` + Code *string `url:"code,omitempty" json:"code,omitempty"` + VisibilityLevel *VisibilityLevelValue `url:"visibility_level,omitempty" json:"visibility_level,omitempty"` +} + +// CreateSnippet creates a new project snippet. The user must have permission +// to create new snippets. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/project_snippets.html#create-new-snippet +func (s *ProjectSnippetsService) CreateSnippet(pid interface{}, opt *CreateSnippetOptions, options ...OptionFunc) (*Snippet, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/snippets", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + ps := new(Snippet) + resp, err := s.client.Do(req, ps) + if err != nil { + return nil, resp, err + } + + return ps, resp, err +} + +// UpdateSnippetOptions represents the available UpdateSnippet() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/project_snippets.html#update-snippet +type UpdateSnippetOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + FileName *string `url:"file_name,omitempty" json:"file_name,omitempty"` + Code *string `url:"code,omitempty" json:"code,omitempty"` + VisibilityLevel *VisibilityLevelValue `url:"visibility_level,omitempty" json:"visibility_level,omitempty"` +} + +// UpdateSnippet updates an existing project snippet. The user must have +// permission to change an existing snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/project_snippets.html#update-snippet +func (s *ProjectSnippetsService) UpdateSnippet(pid interface{}, snippet int, opt *UpdateSnippetOptions, options ...OptionFunc) (*Snippet, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d", url.QueryEscape(project), snippet) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + ps := new(Snippet) + resp, err := s.client.Do(req, ps) + if err != nil { + return nil, resp, err + } + + return ps, resp, err +} + +// DeleteSnippet deletes an existing project snippet. This is an idempotent +// function and deleting a non-existent snippet still returns a 200 OK status +// code. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/project_snippets.html#delete-snippet +func (s *ProjectSnippetsService) DeleteSnippet(pid interface{}, snippet int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d", url.QueryEscape(project), snippet) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// SnippetContent returns the raw project snippet as plain text. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/project_snippets.html#snippet-content +func (s *ProjectSnippetsService) SnippetContent(pid interface{}, snippet int, options ...OptionFunc) ([]byte, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d/raw", url.QueryEscape(project), snippet) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var b bytes.Buffer + resp, err := s.client.Do(req, &b) + if err != nil { + return nil, resp, err + } + + return b.Bytes(), resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/projects.go b/vendor/github.com/xanzy/go-gitlab/projects.go new file mode 100644 index 000000000..9abc6b141 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/projects.go @@ -0,0 +1,981 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// ProjectsService handles communication with the repositories related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html +type ProjectsService struct { + client *Client +} + +// Project represents a GitLab project. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html +type Project struct { + ID int `json:"id"` + Description string `json:"description"` + DefaultBranch string `json:"default_branch"` + Public bool `json:"public"` + VisibilityLevel VisibilityLevelValue `json:"visibility_level"` + SSHURLToRepo string `json:"ssh_url_to_repo"` + HTTPURLToRepo string `json:"http_url_to_repo"` + WebURL string `json:"web_url"` + TagList []string `json:"tag_list"` + Owner *User `json:"owner"` + Name string `json:"name"` + NameWithNamespace string `json:"name_with_namespace"` + Path string `json:"path"` + PathWithNamespace string `json:"path_with_namespace"` + IssuesEnabled bool `json:"issues_enabled"` + OpenIssuesCount int `json:"open_issues_count"` + MergeRequestsEnabled bool `json:"merge_requests_enabled"` + ApprovalsBeforeMerge int `json:"approvals_before_merge"` + BuildsEnabled bool `json:"builds_enabled"` + WikiEnabled bool `json:"wiki_enabled"` + SnippetsEnabled bool `json:"snippets_enabled"` + ContainerRegistryEnabled bool `json:"container_registry_enabled"` + CreatedAt *time.Time `json:"created_at,omitempty"` + LastActivityAt *time.Time `json:"last_activity_at,omitempty"` + CreatorID int `json:"creator_id"` + Namespace *ProjectNamespace `json:"namespace"` + Permissions *Permissions `json:"permissions"` + Archived bool `json:"archived"` + AvatarURL string `json:"avatar_url"` + SharedRunnersEnabled bool `json:"shared_runners_enabled"` + ForksCount int `json:"forks_count"` + StarCount int `json:"star_count"` + RunnersToken string `json:"runners_token"` + PublicBuilds bool `json:"public_builds"` + OnlyAllowMergeIfBuildSucceeds bool `json:"only_allow_merge_if_build_succeeds"` + OnlyAllowMergeIfAllDiscussionsAreResolved bool `json:"only_allow_merge_if_all_discussions_are_resolved"` + LFSEnabled bool `json:"lfs_enabled"` + RequestAccessEnabled bool `json:"request_access_enabled"` + SharedWithGroups []struct { + GroupID int `json:"group_id"` + GroupName string `json:"group_name"` + GroupAccessLevel int `json:"group_access_level"` + } `json:"shared_with_groups"` + Statistics *ProjectStatistics `json:"statistics"` +} + +// Repository represents a repository. +type Repository struct { + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` +} + +// ProjectNamespace represents a project namespace. +type ProjectNamespace struct { + CreatedAt *time.Time `json:"created_at"` + Description string `json:"description"` + ID int `json:"id"` + Name string `json:"name"` + OwnerID int `json:"owner_id"` + Path string `json:"path"` + UpdatedAt *time.Time `json:"updated_at"` +} + +// StorageStatistics represents a statistics record for a group or project. +type StorageStatistics struct { + StorageSize int64 `json:"storage_size"` + RepositorySize int64 `json:"repository_size"` + LfsObjectsSize int64 `json:"lfs_objects_size"` + BuildArtifactsSize int64 `json:"build_artifacts_size"` +} + +// ProjectStatistics represents a statistics record for a project. +type ProjectStatistics struct { + StorageStatistics + CommitCount int `json:"commit_count"` +} + +// Permissions represents premissions. +type Permissions struct { + ProjectAccess *ProjectAccess `json:"project_access"` + GroupAccess *GroupAccess `json:"group_access"` +} + +// ProjectAccess represents project access. +type ProjectAccess struct { + AccessLevel AccessLevelValue `json:"access_level"` + NotificationLevel NotificationLevelValue `json:"notification_level"` +} + +// GroupAccess represents group access. +type GroupAccess struct { + AccessLevel AccessLevelValue `json:"access_level"` + NotificationLevel NotificationLevelValue `json:"notification_level"` +} + +func (s Project) String() string { + return Stringify(s) +} + +// ListProjectsOptions represents the available ListProjects() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#list-projects +type ListProjectsOptions struct { + ListOptions + Archived *bool `url:"archived,omitempty" json:"archived,omitempty"` + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` + Search *string `url:"search,omitempty" json:"search,omitempty"` + Simple *bool `url:"simple,omitempty" json:"simple,omitempty"` + Visibility *string `url:"visibility,omitempty" json:"visibility,omitempty"` + Statistics *bool `url:"statistics,omitempty" json:"statistics,omitempty"` +} + +// ListProjects gets a list of projects accessible by the authenticated user. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#list-projects +func (s *ProjectsService) ListProjects(opt *ListProjectsOptions, options ...OptionFunc) ([]*Project, *Response, error) { + req, err := s.client.NewRequest("GET", "projects", opt, options) + if err != nil { + return nil, nil, err + } + + var p []*Project + resp, err := s.client.Do(req, &p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// ListOwnedProjects gets a list of projects which are owned by the +// authenticated user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#list-owned-projects +func (s *ProjectsService) ListOwnedProjects(opt *ListProjectsOptions, options ...OptionFunc) ([]*Project, *Response, error) { + req, err := s.client.NewRequest("GET", "projects/owned", opt, options) + if err != nil { + return nil, nil, err + } + + var p []*Project + resp, err := s.client.Do(req, &p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// ListStarredProjects gets a list of projects which are starred by the +// authenticated user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#list-starred-projects +func (s *ProjectsService) ListStarredProjects(opt *ListProjectsOptions, options ...OptionFunc) ([]*Project, *Response, error) { + req, err := s.client.NewRequest("GET", "projects/starred", opt, options) + if err != nil { + return nil, nil, err + } + + var p []*Project + resp, err := s.client.Do(req, &p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// ListAllProjects gets a list of all GitLab projects (admin only). +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#list-all-projects +func (s *ProjectsService) ListAllProjects(opt *ListProjectsOptions, options ...OptionFunc) ([]*Project, *Response, error) { + req, err := s.client.NewRequest("GET", "projects/all", opt, options) + if err != nil { + return nil, nil, err + } + + var p []*Project + resp, err := s.client.Do(req, &p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// GetProject gets a specific project, identified by project ID or +// NAMESPACE/PROJECT_NAME, which is owned by the authenticated user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#get-single-project +func (s *ProjectsService) GetProject(pid interface{}, options ...OptionFunc) (*Project, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(Project) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// SearchProjectsOptions represents the available SearchProjects() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#search-for-projects-by-name +type SearchProjectsOptions struct { + ListOptions + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` +} + +// SearchProjects searches for projects by name which are accessible to the +// authenticated user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#search-for-projects-by-name +func (s *ProjectsService) SearchProjects(query string, opt *SearchProjectsOptions, options ...OptionFunc) ([]*Project, *Response, error) { + u := fmt.Sprintf("projects/search/%s", query) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var p []*Project + resp, err := s.client.Do(req, &p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// ProjectEvent represents a GitLab project event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#get-project-events +type ProjectEvent struct { + Title interface{} `json:"title"` + ProjectID int `json:"project_id"` + ActionName string `json:"action_name"` + TargetID interface{} `json:"target_id"` + TargetType interface{} `json:"target_type"` + AuthorID int `json:"author_id"` + AuthorUsername string `json:"author_username"` + Data struct { + Before string `json:"before"` + After string `json:"after"` + Ref string `json:"ref"` + UserID int `json:"user_id"` + UserName string `json:"user_name"` + Repository *Repository `json:"repository"` + Commits []*Commit `json:"commits"` + TotalCommitsCount int `json:"total_commits_count"` + } `json:"data"` + TargetTitle interface{} `json:"target_title"` +} + +func (s ProjectEvent) String() string { + return Stringify(s) +} + +// GetProjectEventsOptions represents the available GetProjectEvents() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#get-project-events +type GetProjectEventsOptions struct { + ListOptions +} + +// GetProjectEvents gets the events for the specified project. Sorted from +// newest to latest. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#get-project-events +func (s *ProjectsService) GetProjectEvents(pid interface{}, opt *GetProjectEventsOptions, options ...OptionFunc) ([]*ProjectEvent, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/events", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var p []*ProjectEvent + resp, err := s.client.Do(req, &p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// CreateProjectOptions represents the available CreateProjects() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#create-project +type CreateProjectOptions struct { + Name *string `url:"name,omitempty" json:"name,omitempty"` + Path *string `url:"path,omitempty" json:"path,omitempty"` + NamespaceID *int `url:"namespace_id,omitempty" json:"namespace_id,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + IssuesEnabled *bool `url:"issues_enabled,omitempty" json:"issues_enabled,omitempty"` + MergeRequestsEnabled *bool `url:"merge_requests_enabled,omitempty" json:"merge_requests_enabled,omitempty"` + BuildsEnabled *bool `url:"builds_enabled,omitempty" json:"builds_enabled,omitempty"` + WikiEnabled *bool `url:"wiki_enabled,omitempty" json:"wiki_enabled,omitempty"` + SnippetsEnabled *bool `url:"snippets_enabled,omitempty" json:"snippets_enabled,omitempty"` + ContainerRegistryEnabled *bool `url:"container_registry_enabled,omitempty" json:"container_registry_enabled,omitempty"` + SharedRunnersEnabled *bool `url:"shared_runners_enabled,omitempty" json:"shared_runners_enabled,omitempty"` + Public *bool `url:"public,omitempty" json:"public,omitempty"` + VisibilityLevel *VisibilityLevelValue `url:"visibility_level,omitempty" json:"visibility_level,omitempty"` + ImportURL *string `url:"import_url,omitempty" json:"import_url,omitempty"` + PublicBuilds *bool `url:"public_builds,omitempty" json:"public_builds,omitempty"` + OnlyAllowMergeIfBuildSucceeds *bool `url:"only_allow_merge_if_build_succeeds,omitempty" json:"only_allow_merge_if_build_succeeds,omitempty"` + LFSEnabled *bool `url:"lfs_enabled,omitempty" json:"lfs_enabled,omitempty"` + RequestAccessEnabled *bool `url:"request_access_enabled,omitempty" json:"request_access_enabled,omitempty"` +} + +// CreateProject creates a new project owned by the authenticated user. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#create-project +func (s *ProjectsService) CreateProject(opt *CreateProjectOptions, options ...OptionFunc) (*Project, *Response, error) { + req, err := s.client.NewRequest("POST", "projects", opt, options) + if err != nil { + return nil, nil, err + } + + p := new(Project) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// CreateProjectForUserOptions represents the available CreateProjectForUser() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#create-project-for-user +type CreateProjectForUserOptions struct { + Name *string `url:"name,omitempty" json:"name,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + DefaultBranch *string `url:"default_branch,omitempty" json:"default_branch,omitempty"` + IssuesEnabled *bool `url:"issues_enabled,omitempty" json:"issues_enabled,omitempty"` + MergeRequestsEnabled *bool `url:"merge_requests_enabled,omitempty" json:"merge_requests_enabled,omitempty"` + WikiEnabled *bool `url:"wiki_enabled,omitempty" json:"wiki_enabled,omitempty"` + SnippetsEnabled *bool `url:"snippets_enabled,omitempty" json:"snippets_enabled,omitempty"` + Public *bool `url:"public,omitempty" json:"public,omitempty"` + VisibilityLevel *VisibilityLevelValue `url:"visibility_level,omitempty" json:"visibility_level,omitempty"` + ImportURL *string `url:"import_url,omitempty" json:"import_url,omitempty"` +} + +// CreateProjectForUser creates a new project owned by the specified user. +// Available only for admins. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#create-project-for-user +func (s *ProjectsService) CreateProjectForUser(user int, opt *CreateProjectForUserOptions, options ...OptionFunc) (*Project, *Response, error) { + u := fmt.Sprintf("projects/user/%d", user) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + p := new(Project) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// EditProjectOptions represents the available EditProject() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#edit-project +type EditProjectOptions struct { + Name *string `url:"name,omitempty" json:"name,omitempty"` + Path *string `url:"path,omitempty" json:"path,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + DefaultBranch *string `url:"default_branch,omitempty" json:"default_branch,omitempty"` + IssuesEnabled *bool `url:"issues_enabled,omitempty" json:"issues_enabled,omitempty"` + MergeRequestsEnabled *bool `url:"merge_requests_enabled,omitempty" json:"merge_requests_enabled,omitempty"` + ApprovalsBeforeMerge *int `url:"approvals_before_merge,omitempty" json:"approvals_before_merge,omitempty"` + BuildsEnabled *bool `url:"builds_enabled,omitempty" json:"builds_enabled,omitempty"` + WikiEnabled *bool `url:"wiki_enabled,omitempty" json:"wiki_enabled,omitempty"` + SnippetsEnabled *bool `url:"snippets_enabled,omitempty" json:"snippets_enabled,omitempty"` + ContainerRegistryEnabled *bool `url:"container_registry_enabled,omitempty" json:"container_registry_enabled,omitempty"` + SharedRunnersEnabled *bool `url:"shared_runners_enabled,omitempty" json:"shared_runners_enabled,omitempty"` + Public *bool `url:"public,omitempty" json:"public,omitempty"` + VisibilityLevel *VisibilityLevelValue `url:"visibility_level,omitempty" json:"visibility_level,omitempty"` + ImportURL *bool `url:"import_url,omitempty" json:"import_url,omitempty"` + PublicBuilds *bool `url:"public_builds,omitempty" json:"public_builds,omitempty"` + OnlyAllowMergeIfBuildSucceeds *bool `url:"only_allow_merge_if_build_succeeds,omitempty" json:"only_allow_merge_if_build_succeeds,omitempty"` + OnlyAllowMergeIfAllDiscussionsAreResolved *bool `url:"only_allow_merge_if_all_discussions_are_resolved,omitempty" json:"only_allow_merge_if_all_discussions_are_resolved,omitempty"` + LFSEnabled *bool `url:"lfs_enabled,omitempty" json:"lfs_enabled,omitempty"` + RequestAccessEnabled *bool `url:"request_access_enabled,omitempty" json:"request_access_enabled,omitempty"` +} + +// EditProject updates an existing project. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#edit-project +func (s *ProjectsService) EditProject(pid interface{}, opt *EditProjectOptions, options ...OptionFunc) (*Project, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s", url.QueryEscape(project)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + p := new(Project) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// ForkProject forks a project into the user namespace of the authenticated +// user. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#fork-project +func (s *ProjectsService) ForkProject(pid interface{}, options ...OptionFunc) (*Project, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/fork/%s", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(Project) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// DeleteProject removes a project including all associated resources +// (issues, merge requests etc.) +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#remove-project +func (s *ProjectsService) DeleteProject(pid interface{}, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s", url.QueryEscape(project)) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// ProjectMember represents a project member. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#list-project-team-members +type ProjectMember struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + AccessLevel AccessLevelValue `json:"access_level"` +} + +// ListProjectMembersOptions represents the available ListProjectMembers() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#list-project-team-members +type ListProjectMembersOptions struct { + ListOptions + Query *string `url:"query,omitempty" json:"query,omitempty"` +} + +// ListProjectMembers gets a list of a project's team members. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#list-project-team-members +func (s *ProjectsService) ListProjectMembers(pid interface{}, opt *ListProjectMembersOptions, options ...OptionFunc) ([]*ProjectMember, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/members", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var pm []*ProjectMember + resp, err := s.client.Do(req, &pm) + if err != nil { + return nil, resp, err + } + + return pm, resp, err +} + +// GetProjectMember gets a project team member. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#get-project-team-member +func (s *ProjectsService) GetProjectMember(pid interface{}, user int, options ...OptionFunc) (*ProjectMember, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/members/%d", url.QueryEscape(project), user) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + pm := new(ProjectMember) + resp, err := s.client.Do(req, pm) + if err != nil { + return nil, resp, err + } + + return pm, resp, err +} + +// AddProjectMemberOptions represents the available AddProjectMember() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#add-project-team-member +type AddProjectMemberOptions struct { + UserID *int `url:"user_id,omitempty" json:"user_id,omitempty"` + AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"` +} + +// AddProjectMember adds a user to a project team. This is an idempotent +// method and can be called multiple times with the same parameters. Adding +// team membership to a user that is already a member does not affect the +// existing membership. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#add-project-team-member +func (s *ProjectsService) AddProjectMember(pid interface{}, opt *AddProjectMemberOptions, options ...OptionFunc) (*ProjectMember, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/members", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + pm := new(ProjectMember) + resp, err := s.client.Do(req, pm) + if err != nil { + return nil, resp, err + } + + return pm, resp, err +} + +// EditProjectMemberOptions represents the available EditProjectMember() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#edit-project-team-member +type EditProjectMemberOptions struct { + AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"` +} + +// EditProjectMember updates a project team member to a specified access level.. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#edit-project-team-member +func (s *ProjectsService) EditProjectMember(pid interface{}, user int, opt *EditProjectMemberOptions, options ...OptionFunc) (*ProjectMember, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/members/%d", url.QueryEscape(project), user) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + pm := new(ProjectMember) + resp, err := s.client.Do(req, pm) + if err != nil { + return nil, resp, err + } + + return pm, resp, err +} + +// DeleteProjectMember removes a user from a project team. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#remove-project-team-member +func (s *ProjectsService) DeleteProjectMember(pid interface{}, user int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/members/%d", url.QueryEscape(project), user) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// ProjectHook represents a project hook. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#list-project-hooks +type ProjectHook struct { + ID int `json:"id"` + URL string `json:"url"` + ProjectID int `json:"project_id"` + PushEvents bool `json:"push_events"` + IssuesEvents bool `json:"issues_events"` + MergeRequestsEvents bool `json:"merge_requests_events"` + TagPushEvents bool `json:"tag_push_events"` + NoteEvents bool `json:"note_events"` + BuildEvents bool `json:"build_events"` + PipelineEvents bool `json:"pipeline_events"` + WikiPageEvents bool `json:"wiki_page_events"` + EnableSSLVerification bool `json:"enable_ssl_verification"` + CreatedAt *time.Time `json:"created_at"` +} + +// ListProjectHooksOptions represents the available ListProjectHooks() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#list-project-hooks +type ListProjectHooksOptions struct { + ListOptions +} + +// ListProjectHooks gets a list of project hooks. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#list-project-hooks +func (s *ProjectsService) ListProjectHooks(pid interface{}, opt *ListProjectHooksOptions, options ...OptionFunc) ([]*ProjectHook, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/hooks", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var ph []*ProjectHook + resp, err := s.client.Do(req, &ph) + if err != nil { + return nil, resp, err + } + + return ph, resp, err +} + +// GetProjectHook gets a specific hook for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#get-project-hook +func (s *ProjectsService) GetProjectHook(pid interface{}, hook int, options ...OptionFunc) (*ProjectHook, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/hooks/%d", url.QueryEscape(project), hook) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + ph := new(ProjectHook) + resp, err := s.client.Do(req, ph) + if err != nil { + return nil, resp, err + } + + return ph, resp, err +} + +// AddProjectHookOptions represents the available AddProjectHook() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#add-project-hook +type AddProjectHookOptions struct { + URL *string `url:"url,omitempty" json:"url,omitempty"` + PushEvents *bool `url:"push_events,omitempty" json:"push_events,omitempty"` + IssuesEvents *bool `url:"issues_events,omitempty" json:"issues_events,omitempty"` + MergeRequestsEvents *bool `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"` + TagPushEvents *bool `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"` + NoteEvents *bool `url:"note_events,omitempty" json:"note_events,omitempty"` + BuildEvents *bool `url:"build_events,omitempty" json:"build_events,omitempty"` + PipelineEvents *bool `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"` + WikiPageEvents *bool `url:"wiki_page_events,omitempty" json:"wiki_page_events,omitempty"` + EnableSSLVerification *bool `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"` + Token *string `url:"token,omitempty" json:"token,omitempty"` +} + +// AddProjectHook adds a hook to a specified project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#add-project-hook +func (s *ProjectsService) AddProjectHook(pid interface{}, opt *AddProjectHookOptions, options ...OptionFunc) (*ProjectHook, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/hooks", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + ph := new(ProjectHook) + resp, err := s.client.Do(req, ph) + if err != nil { + return nil, resp, err + } + + return ph, resp, err +} + +// EditProjectHookOptions represents the available EditProjectHook() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#edit-project-hook +type EditProjectHookOptions struct { + URL *string `url:"url,omitempty" json:"url,omitempty"` + PushEvents *bool `url:"push_events,omitempty" json:"push_events,omitempty"` + IssuesEvents *bool `url:"issues_events,omitempty" json:"issues_events,omitempty"` + MergeRequestsEvents *bool `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"` + TagPushEvents *bool `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"` + NoteEvents *bool `url:"note_events,omitempty" json:"note_events,omitempty"` + BuildEvents *bool `url:"build_events,omitempty" json:"build_events,omitempty"` + PipelineEvents *bool `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"` + WikiPageEvents *bool `url:"wiki_page_events,omitempty" json:"wiki_page_events,omitempty"` + EnableSSLVerification *bool `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"` + Token *string `url:"token,omitempty" json:"token,omitempty"` +} + +// EditProjectHook edits a hook for a specified project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#edit-project-hook +func (s *ProjectsService) EditProjectHook(pid interface{}, hook int, opt *EditProjectHookOptions, options ...OptionFunc) (*ProjectHook, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/hooks/%d", url.QueryEscape(project), hook) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + ph := new(ProjectHook) + resp, err := s.client.Do(req, ph) + if err != nil { + return nil, resp, err + } + + return ph, resp, err +} + +// DeleteProjectHook removes a hook from a project. This is an idempotent +// method and can be called multiple times. Either the hook is available or not. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#delete-project-hook +func (s *ProjectsService) DeleteProjectHook(pid interface{}, hook int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/hooks/%d", url.QueryEscape(project), hook) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// ProjectForkRelation represents a project fork relationship. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#admin-fork-relation +type ProjectForkRelation struct { + ID int `json:"id"` + ForkedToProjectID int `json:"forked_to_project_id"` + ForkedFromProjectID int `json:"forked_from_project_id"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` +} + +// CreateProjectForkRelation creates a forked from/to relation between +// existing projects. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#create-a-forked-fromto-relation-between-existing-projects. +func (s *ProjectsService) CreateProjectForkRelation(pid int, fork int, options ...OptionFunc) (*ProjectForkRelation, *Response, error) { + u := fmt.Sprintf("projects/%d/fork/%d", pid, fork) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + pfr := new(ProjectForkRelation) + resp, err := s.client.Do(req, pfr) + if err != nil { + return nil, resp, err + } + + return pfr, resp, err +} + +// DeleteProjectForkRelation deletes an existing forked from relationship. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#delete-an-existing-forked-from-relationship +func (s *ProjectsService) DeleteProjectForkRelation(pid int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("projects/%d/fork", pid) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// ArchiveProject archives the project if the user is either admin or the +// project owner of this project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#archive-a-project +func (s *ProjectsService) ArchiveProject(pid interface{}, options ...OptionFunc) (*Project, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/archive", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(Project) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// UnarchiveProject unarchives the project if the user is either admin or +// the project owner of this project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#unarchive-a-project +func (s *ProjectsService) UnarchiveProject(pid interface{}, options ...OptionFunc) (*Project, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/unarchive", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(Project) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/repositories.go b/vendor/github.com/xanzy/go-gitlab/repositories.go new file mode 100644 index 000000000..aa1052ff6 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/repositories.go @@ -0,0 +1,259 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "bytes" + "fmt" + "net/url" +) + +// RepositoriesService handles communication with the repositories related +// methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html +type RepositoriesService struct { + client *Client +} + +// TreeNode represents a GitLab repository file or directory. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html +type TreeNode struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Mode string `json:"mode"` +} + +func (t TreeNode) String() string { + return Stringify(t) +} + +// ListTreeOptions represents the available ListTree() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repositories.html#list-repository-tree +type ListTreeOptions struct { + Path *string `url:"path,omitempty" json:"path,omitempty"` + RefName *string `url:"ref_name,omitempty" json:"ref_name,omitempty"` +} + +// ListTree gets a list of repository files and directories in a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repositories.html#list-repository-tree +func (s *RepositoriesService) ListTree(pid interface{}, opt *ListTreeOptions, options ...OptionFunc) ([]*TreeNode, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/tree", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var t []*TreeNode + resp, err := s.client.Do(req, &t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// RawFileContentOptions represents the available RawFileContent() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repositories.html#raw-file-content +type RawFileContentOptions struct { + FilePath *string `url:"filepath,omitempty" json:"filepath,omitempty"` +} + +// RawFileContent gets the raw file contents for a file by commit SHA and path +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repositories.html#raw-file-content +func (s *RepositoriesService) RawFileContent(pid interface{}, sha string, opt *RawFileContentOptions, options ...OptionFunc) ([]byte, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/blobs/%s", url.QueryEscape(project), sha) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var b bytes.Buffer + resp, err := s.client.Do(req, &b) + if err != nil { + return nil, resp, err + } + + return b.Bytes(), resp, err +} + +// RawBlobContent gets the raw file contents for a blob by blob SHA. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repositories.html#raw-blob-content +func (s *RepositoriesService) RawBlobContent(pid interface{}, sha string, options ...OptionFunc) ([]byte, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/raw_blobs/%s", url.QueryEscape(project), sha) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var b bytes.Buffer + resp, err := s.client.Do(req, &b) + if err != nil { + return nil, resp, err + } + + return b.Bytes(), resp, err +} + +// ArchiveOptions represents the available Archive() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repositories.html#get-file-archive +type ArchiveOptions struct { + SHA *string `url:"sha,omitempty" json:"sha,omitempty"` +} + +// Archive gets an archive of the repository. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repositories.html#get-file-archive +func (s *RepositoriesService) Archive(pid interface{}, opt *ArchiveOptions, options ...OptionFunc) ([]byte, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/archive", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var b bytes.Buffer + resp, err := s.client.Do(req, &b) + if err != nil { + return nil, resp, err + } + + return b.Bytes(), resp, err +} + +// Compare represents the result of a comparison of branches, tags or commits. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits +type Compare struct { + Commit *Commit `json:"commit"` + Commits []*Commit `json:"commits"` + Diffs []*Diff `json:"diffs"` + CompareTimeout bool `json:"compare_timeout"` + CompareSameRef bool `json:"compare_same_ref"` +} + +func (c Compare) String() string { + return Stringify(c) +} + +// CompareOptions represents the available Compare() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits +type CompareOptions struct { + From *string `url:"from,omitempty" json:"from,omitempty"` + To *string `url:"to,omitempty" json:"to,omitempty"` +} + +// Compare compares branches, tags or commits. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits +func (s *RepositoriesService) Compare(pid interface{}, opt *CompareOptions, options ...OptionFunc) (*Compare, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/compare", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + c := new(Compare) + resp, err := s.client.Do(req, c) + if err != nil { + return nil, resp, err + } + + return c, resp, err +} + +// Contributor represents a GitLap contributor. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html#contributer +type Contributor struct { + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty"` + Commits int `json:"commits,omitempty"` + Additions int `json:"additions,omitempty"` + Deletions int `json:"deletions,omitempty"` +} + +func (c Contributor) String() string { + return Stringify(c) +} + +// Contributors gets the repository contributors list. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html#contributer +func (s *RepositoriesService) Contributors(pid interface{}, options ...OptionFunc) ([]*Contributor, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/contributors", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var c []*Contributor + resp, err := s.client.Do(req, &c) + if err != nil { + return nil, resp, err + } + + return c, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/repository_files.go b/vendor/github.com/xanzy/go-gitlab/repository_files.go new file mode 100644 index 000000000..21e5f65cf --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/repository_files.go @@ -0,0 +1,210 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// RepositoryFilesService handles communication with the repository files +// related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/repository_files.html +type RepositoryFilesService struct { + client *Client +} + +// File represents a GitLab repository file. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/repository_files.html +type File struct { + FileName string `json:"file_name"` + FilePath string `json:"file_path"` + Size int `json:"size"` + Encoding string `json:"encoding"` + Content string `json:"content"` + Ref string `json:"ref"` + BlobID string `json:"blob_id"` + CommitID string `json:"commit_id"` +} + +func (r File) String() string { + return Stringify(r) +} + +// GetFileOptions represents the available GetFile() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repository_files.html#get-file-from-respository +type GetFileOptions struct { + FilePath *string `url:"file_path,omitempty" json:"file_path,omitempty"` + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` +} + +// GetFile allows you to receive information about a file in repository like +// name, size, content. Note that file content is Base64 encoded. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repository_files.html#get-file-from-respository +func (s *RepositoryFilesService) GetFile(pid interface{}, opt *GetFileOptions, options ...OptionFunc) (*File, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/files", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + f := new(File) + resp, err := s.client.Do(req, f) + if err != nil { + return nil, resp, err + } + + return f, resp, err +} + +// FileInfo represents file details of a GitLab repository file. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/repository_files.html +type FileInfo struct { + FilePath string `json:"file_path"` + BranchName string `json:"branch_name"` +} + +func (r FileInfo) String() string { + return Stringify(r) +} + +// CreateFileOptions represents the available CreateFile() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repository_files.html#create-new-file-in-repository +type CreateFileOptions struct { + FilePath *string `url:"file_path,omitempty" json:"file_path,omitempty"` + BranchName *string `url:"branch_name,omitempty" json:"branch_name,omitempty"` + Encoding *string `url:"encoding,omitempty" json:"encoding,omitempty"` + AuthorEmail *string `url:"author_email,omitempty" json:"author_email,omitempty"` + AuthorName *string `url:"author_name,omitempty" json:"author_name,omitempty"` + Content *string `url:"content,omitempty" json:"content,omitempty"` + CommitMessage *string `url:"commit_message,omitempty" json:"commit_message,omitempty"` +} + +// CreateFile creates a new file in a repository. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repository_files.html#create-new-file-in-repository +func (s *RepositoryFilesService) CreateFile(pid interface{}, opt *CreateFileOptions, options ...OptionFunc) (*FileInfo, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/files", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + f := new(FileInfo) + resp, err := s.client.Do(req, f) + if err != nil { + return nil, resp, err + } + + return f, resp, err +} + +// UpdateFileOptions represents the available UpdateFile() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repository_files.html#update-existing-file-in-repository +type UpdateFileOptions struct { + FilePath *string `url:"file_path,omitempty" json:"file_path,omitempty"` + BranchName *string `url:"branch_name,omitempty" json:"branch_name,omitempty"` + Encoding *string `url:"encoding,omitempty" json:"encoding,omitempty"` + AuthorEmail *string `url:"author_email,omitempty" json:"author_email,omitempty"` + AuthorName *string `url:"author_name,omitempty" json:"author_name,omitempty"` + Content *string `url:"content,omitempty" json:"content,omitempty"` + CommitMessage *string `url:"commit_message,omitempty" json:"commit_message,omitempty"` +} + +// UpdateFile updates an existing file in a repository +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repository_files.html#update-existing-file-in-repository +func (s *RepositoryFilesService) UpdateFile(pid interface{}, opt *UpdateFileOptions, options ...OptionFunc) (*FileInfo, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/files", url.QueryEscape(project)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + f := new(FileInfo) + resp, err := s.client.Do(req, f) + if err != nil { + return nil, resp, err + } + + return f, resp, err +} + +// DeleteFileOptions represents the available DeleteFile() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repository_files.html#delete-existing-file-in-repository +type DeleteFileOptions struct { + FilePath *string `url:"file_path,omitempty" json:"file_path,omitempty"` + BranchName *string `url:"branch_name,omitempty" json:"branch_name,omitempty"` + AuthorEmail *string `url:"author_email,omitempty" json:"author_email,omitempty"` + AuthorName *string `url:"author_name,omitempty" json:"author_name,omitempty"` + CommitMessage *string `url:"commit_message,omitempty" json:"commit_message,omitempty"` +} + +// DeleteFile deletes an existing file in a repository +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repository_files.html#delete-existing-file-in-repository +func (s *RepositoryFilesService) DeleteFile(pid interface{}, opt *DeleteFileOptions, options ...OptionFunc) (*FileInfo, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/files", url.QueryEscape(project)) + + req, err := s.client.NewRequest("DELETE", u, opt, options) + if err != nil { + return nil, nil, err + } + + f := new(FileInfo) + resp, err := s.client.Do(req, f) + if err != nil { + return nil, resp, err + } + + return f, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/services.go b/vendor/github.com/xanzy/go-gitlab/services.go new file mode 100644 index 000000000..dbcdf84ab --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/services.go @@ -0,0 +1,279 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// ServicesService handles communication with the services related methods of +// the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/services.html +type ServicesService struct { + client *Client +} + +// Service represents a GitLab service. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/services.html +type Service struct { + ID *int `json:"id"` + Title *string `json:"title"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"created_at"` + Active *bool `json:"active"` + PushEvents *bool `json:"push_events"` + IssuesEvents *bool `json:"issues_events"` + MergeRequestsEvents *bool `json:"merge_requests_events"` + TagPushEvents *bool `json:"tag_push_events"` + NoteEvents *bool `json:"note_events"` +} + +// SetGitLabCIServiceOptions represents the available SetGitLabCIService() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#edit-gitlab-ci-service +type SetGitLabCIServiceOptions struct { + Token *string `url:"token,omitempty" json:"token,omitempty"` + ProjectURL *string `url:"project_url,omitempty" json:"project_url,omitempty"` +} + +// SetGitLabCIService sets GitLab CI service for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#edit-gitlab-ci-service +func (s *ServicesService) SetGitLabCIService(pid interface{}, opt *SetGitLabCIServiceOptions, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/gitlab-ci", url.QueryEscape(project)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DeleteGitLabCIService deletes GitLab CI service settings for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#delete-gitlab-ci-service +func (s *ServicesService) DeleteGitLabCIService(pid interface{}, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/gitlab-ci", url.QueryEscape(project)) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// SetHipChatServiceOptions represents the available SetHipChatService() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#edit-hipchat-service +type SetHipChatServiceOptions struct { + Token *string `url:"token,omitempty" json:"token,omitempty" ` + Room *string `url:"room,omitempty" json:"room,omitempty"` +} + +// SetHipChatService sets HipChat service for a project +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#edit-hipchat-service +func (s *ServicesService) SetHipChatService(pid interface{}, opt *SetHipChatServiceOptions, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/hipchat", url.QueryEscape(project)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DeleteHipChatService deletes HipChat service for project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#delete-hipchat-service +func (s *ServicesService) DeleteHipChatService(pid interface{}, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/hipchat", url.QueryEscape(project)) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// SetDroneCIServiceOptions represents the available SetDroneCIService() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#createedit-drone-ci-service +type SetDroneCIServiceOptions struct { + Token *string `url:"token" json:"token" ` + DroneURL *string `url:"drone_url" json:"drone_url"` + EnableSSLVerification *string `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"` +} + +// SetDroneCIService sets Drone CI service for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#createedit-drone-ci-service +func (s *ServicesService) SetDroneCIService(pid interface{}, opt *SetDroneCIServiceOptions, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/drone-ci", url.QueryEscape(project)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DeleteDroneCIService deletes Drone CI service settings for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#delete-drone-ci-service +func (s *ServicesService) DeleteDroneCIService(pid interface{}, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/drone-ci", url.QueryEscape(project)) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DroneCIServiceProperties represents Drone CI specific properties. +type DroneCIServiceProperties struct { + Token *string `url:"token" json:"token"` + DroneURL *string `url:"drone_url" json:"drone_url"` + EnableSSLVerification *string `url:"enable_ssl_verification" json:"enable_ssl_verification"` +} + +// DroneCIService represents Drone CI service settings. +type DroneCIService struct { + Service + Properties *DroneCIServiceProperties `json:"properties"` +} + +// GetDroneCIService gets Drone CI service settings for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#get-drone-ci-service-settings +func (s *ServicesService) GetDroneCIService(pid interface{}, options ...OptionFunc) (*DroneCIService, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/services/drone-ci", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + opt := new(DroneCIService) + resp, err := s.client.Do(req, opt) + if err != nil { + return nil, resp, err + } + + return opt, resp, err +} + +// SetSlackServiceOptions represents the available SetSlackService() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#edit-slack-service +type SetSlackServiceOptions struct { + WebHook *string `url:"webhook,omitempty" json:"webhook,omitempty" ` + Username *string `url:"username,omitempty" json:"username,omitempty" ` + Channel *string `url:"channel,omitempty" json:"channel,omitempty"` +} + +// SetSlackService sets Slack service for a project +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#edit-slack-service +func (s *ServicesService) SetSlackService(pid interface{}, opt *SetSlackServiceOptions, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/slack", url.QueryEscape(project)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DeleteSlackService deletes Slack service for project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#delete-slack-service +func (s *ServicesService) DeleteSlackService(pid interface{}, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/slack", url.QueryEscape(project)) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/vendor/github.com/xanzy/go-gitlab/session.go b/vendor/github.com/xanzy/go-gitlab/session.go new file mode 100644 index 000000000..571483cd3 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/session.go @@ -0,0 +1,78 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import "time" + +// SessionService handles communication with the session related methods of +// the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/session.html +type SessionService struct { + client *Client +} + +// Session represents a GitLab session. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/session.html#session +type Session struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + PrivateToken string `json:"private_token"` + Blocked bool `json:"blocked"` + CreatedAt *time.Time `json:"created_at"` + Bio interface{} `json:"bio"` + Skype string `json:"skype"` + Linkedin string `json:"linkedin"` + Twitter string `json:"twitter"` + WebsiteURL string `json:"website_url"` + DarkScheme bool `json:"dark_scheme"` + ThemeID int `json:"theme_id"` + IsAdmin bool `json:"is_admin"` + CanCreateGroup bool `json:"can_create_group"` + CanCreateTeam bool `json:"can_create_team"` + CanCreateProject bool `json:"can_create_project"` +} + +// GetSessionOptions represents the available Session() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/session.html#session +type GetSessionOptions struct { + Login *string `url:"login,omitempty" json:"login,omitempty"` + Email *string `url:"email,omitempty" json:"email,omitempty"` + Password *string `url:"password,omitempty" json:"password,omitempty"` +} + +// GetSession logs in to get private token. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/session.html#session +func (s *SessionService) GetSession(opt *GetSessionOptions, options ...OptionFunc) (*Session, *Response, error) { + req, err := s.client.NewRequest("POST", "session", opt, options) + if err != nil { + return nil, nil, err + } + + session := new(Session) + resp, err := s.client.Do(req, session) + if err != nil { + return nil, resp, err + } + + return session, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/settings.go b/vendor/github.com/xanzy/go-gitlab/settings.go new file mode 100644 index 000000000..41e9c9dab --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/settings.go @@ -0,0 +1,117 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import "time" + +// SettingsService handles communication with the application SettingsService +// related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/settings.html +type SettingsService struct { + client *Client +} + +// Settings represents the GitLab application settings. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/settings.html +type Settings struct { + ID int `json:"id"` + DefaultProjectsLimit int `json:"default_projects_limit"` + SignupEnabled bool `json:"signup_enabled"` + SigninEnabled bool `json:"signin_enabled"` + GravatarEnabled bool `json:"gravatar_enabled"` + SignInText string `json:"sign_in_text"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + HomePageURL string `json:"home_page_url"` + DefaultBranchProtection int `json:"default_branch_protection"` + TwitterSharingEnabled bool `json:"twitter_sharing_enabled"` + RestrictedVisibilityLevels []VisibilityLevelValue `json:"restricted_visibility_levels"` + MaxAttachmentSize int `json:"max_attachment_size"` + SessionExpireDelay int `json:"session_expire_delay"` + DefaultProjectVisibility int `json:"default_project_visibility"` + DefaultSnippetVisibility int `json:"default_snippet_visibility"` + RestrictedSignupDomains []string `json:"restricted_signup_domains"` + UserOauthApplications bool `json:"user_oauth_applications"` + AfterSignOutPath string `json:"after_sign_out_path"` +} + +func (s Settings) String() string { + return Stringify(s) +} + +// GetSettings gets the current application settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/settings.html#get-current-application.settings +func (s *SettingsService) GetSettings(options ...OptionFunc) (*Settings, *Response, error) { + req, err := s.client.NewRequest("GET", "application/settings", nil, options) + if err != nil { + return nil, nil, err + } + + as := new(Settings) + resp, err := s.client.Do(req, as) + if err != nil { + return nil, resp, err + } + + return as, resp, err +} + +// UpdateSettingsOptions represents the available UpdateSettings() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/settings.html#change-application.settings +type UpdateSettingsOptions struct { + DefaultProjectsLimit *int `url:"default_projects_limit,omitempty" json:"default_projects_limit,omitempty"` + SignupEnabled *bool `url:"signup_enabled,omitempty" json:"signup_enabled,omitempty"` + SigninEnabled *bool `url:"signin_enabled,omitempty" json:"signin_enabled,omitempty"` + GravatarEnabled *bool `url:"gravatar_enabled,omitempty" json:"gravatar_enabled,omitempty"` + SignInText *string `url:"sign_in_text,omitempty" json:"sign_in_text,omitempty"` + HomePageURL *string `url:"home_page_url,omitempty" json:"home_page_url,omitempty"` + DefaultBranchProtection *int `url:"default_branch_protection,omitempty" json:"default_branch_protection,omitempty"` + TwitterSharingEnabled *bool `url:"twitter_sharing_enabled,omitempty" json:"twitter_sharing_enabled,omitempty"` + RestrictedVisibilityLevels []VisibilityLevelValue `url:"restricted_visibility_levels,omitempty" json:"restricted_visibility_levels,omitempty"` + MaxAttachmentSize *int `url:"max_attachment_size,omitempty" json:"max_attachment_size,omitempty"` + SessionExpireDelay *int `url:"session_expire_delay,omitempty" json:"session_expire_delay,omitempty"` + DefaultProjectVisibility *int `url:"default_project_visibility,omitempty" json:"default_project_visibility,omitempty"` + DefaultSnippetVisibility *int `url:"default_snippet_visibility,omitempty" json:"default_snippet_visibility,omitempty"` + RestrictedSignupDomains []string `url:"restricted_signup_domains,omitempty" json:"restricted_signup_domains,omitempty"` + UserOauthApplications *bool `url:"user_oauth_applications,omitempty" json:"user_oauth_applications,omitempty"` + AfterSignOutPath *string `url:"after_sign_out_path,omitempty" json:"after_sign_out_path,omitempty"` +} + +// UpdateSettings updates the application settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/settings.html#change-application.settings +func (s *SettingsService) UpdateSettings(opt *UpdateSettingsOptions, options ...OptionFunc) (*Settings, *Response, error) { + req, err := s.client.NewRequest("PUT", "application/settings", opt, options) + if err != nil { + return nil, nil, err + } + + as := new(Settings) + resp, err := s.client.Do(req, as) + if err != nil { + return nil, resp, err + } + + return as, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/strings.go b/vendor/github.com/xanzy/go-gitlab/strings.go new file mode 100644 index 000000000..d0e7679b3 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/strings.go @@ -0,0 +1,94 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "bytes" + "fmt" + + "reflect" +) + +// Stringify attempts to create a reasonable string representation of types in +// the GitHub library. It does things like resolve pointers to their values +// and omits struct fields with nil values. +func Stringify(message interface{}) string { + var buf bytes.Buffer + v := reflect.ValueOf(message) + stringifyValue(&buf, v) + return buf.String() +} + +// stringifyValue was heavily inspired by the goprotobuf library. +func stringifyValue(buf *bytes.Buffer, val reflect.Value) { + if val.Kind() == reflect.Ptr && val.IsNil() { + buf.WriteString("") + return + } + + v := reflect.Indirect(val) + + switch v.Kind() { + case reflect.String: + fmt.Fprintf(buf, `"%s"`, v) + case reflect.Slice: + buf.WriteByte('[') + for i := 0; i < v.Len(); i++ { + if i > 0 { + buf.WriteByte(' ') + } + + stringifyValue(buf, v.Index(i)) + } + + buf.WriteByte(']') + return + case reflect.Struct: + if v.Type().Name() != "" { + buf.WriteString(v.Type().String()) + } + + buf.WriteByte('{') + + var sep bool + for i := 0; i < v.NumField(); i++ { + fv := v.Field(i) + if fv.Kind() == reflect.Ptr && fv.IsNil() { + continue + } + if fv.Kind() == reflect.Slice && fv.IsNil() { + continue + } + + if sep { + buf.WriteString(", ") + } else { + sep = true + } + + buf.WriteString(v.Type().Field(i).Name) + buf.WriteByte(':') + stringifyValue(buf, fv) + } + + buf.WriteByte('}') + default: + if v.CanInterface() { + fmt.Fprint(buf, v.Interface()) + } + } +} diff --git a/vendor/github.com/xanzy/go-gitlab/system_hooks.go b/vendor/github.com/xanzy/go-gitlab/system_hooks.go new file mode 100644 index 000000000..20277a9af --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/system_hooks.go @@ -0,0 +1,143 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "time" +) + +// SystemHooksService handles communication with the system hooks related +// methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/system_hooks.html +type SystemHooksService struct { + client *Client +} + +// Hook represents a GitLap system hook. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/system_hooks.html +type Hook struct { + ID int `json:"id"` + URL string `json:"url"` + CreatedAt *time.Time `json:"created_at"` +} + +func (h Hook) String() string { + return Stringify(h) +} + +// ListHooks gets a list of system hooks. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/system_hooks.html#list-system-hooks +func (s *SystemHooksService) ListHooks(options ...OptionFunc) ([]*Hook, *Response, error) { + req, err := s.client.NewRequest("GET", "hooks", nil, options) + if err != nil { + return nil, nil, err + } + + var h []*Hook + resp, err := s.client.Do(req, &h) + if err != nil { + return nil, resp, err + } + + return h, resp, err +} + +// AddHookOptions represents the available AddHook() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/system_hooks.html#add-new-system-hook-hook +type AddHookOptions struct { + URL *string `url:"url,omitempty" json:"url,omitempty"` +} + +// AddHook adds a new system hook hook. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/system_hooks.html#add-new-system-hook-hook +func (s *SystemHooksService) AddHook(opt *AddHookOptions, options ...OptionFunc) (*Hook, *Response, error) { + req, err := s.client.NewRequest("POST", "hooks", opt, options) + if err != nil { + return nil, nil, err + } + + h := new(Hook) + resp, err := s.client.Do(req, h) + if err != nil { + return nil, resp, err + } + + return h, resp, err +} + +// HookEvent represents an event triggert by a GitLab system hook. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/system_hooks.html +type HookEvent struct { + EventName string `json:"event_name"` + Name string `json:"name"` + Path string `json:"path"` + ProjectID int `json:"project_id"` + OwnerName string `json:"owner_name"` + OwnerEmail string `json:"owner_email"` +} + +func (h HookEvent) String() string { + return Stringify(h) +} + +// TestHook tests a system hook. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/system_hooks.html#test-system-hook +func (s *SystemHooksService) TestHook(hook int, options ...OptionFunc) (*HookEvent, *Response, error) { + u := fmt.Sprintf("hooks/%d", hook) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + h := new(HookEvent) + resp, err := s.client.Do(req, h) + if err != nil { + return nil, resp, err + } + + return h, resp, err +} + +// DeleteHook deletes a system hook. This is an idempotent API function and +// returns 200 OK even if the hook is not available. If the hook is deleted it +// is also returned as JSON. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/system_hooks.html#delete-system-hook +func (s *SystemHooksService) DeleteHook(hook int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("hooks/%d", hook) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/vendor/github.com/xanzy/go-gitlab/tags.go b/vendor/github.com/xanzy/go-gitlab/tags.go new file mode 100644 index 000000000..196f6d454 --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/tags.go @@ -0,0 +1,149 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// TagsService handles communication with the tags related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/tags.html +type TagsService struct { + client *Client +} + +// Tag represents a GitLab tag. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/tags.html +type Tag struct { + Commit *Commit `json:"commit"` + Name string `json:"name"` + Message string `json:"message"` +} + +func (r Tag) String() string { + return Stringify(r) +} + +// ListTags gets a list of tags from a project, sorted by name in reverse +// alphabetical order. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#list-project-repository-tags +func (s *TagsService) ListTags(pid interface{}, options ...OptionFunc) ([]*Tag, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/tags", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var t []*Tag + resp, err := s.client.Do(req, &t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// GetTag a specific repository tag determined by its name. It returns 200 together +// with the tag information if the tag exists. It returns 404 if the tag does not exist. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#get-a-single-repository-tag +func (s *TagsService) GetTag(pid interface{}, tag string, options ...OptionFunc) (*Tag, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/tags/%s", url.QueryEscape(project), tag) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var t *Tag + resp, err := s.client.Do(req, &t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// CreateTagOptions represents the available CreateTag() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#create-a-new-tag +type CreateTagOptions struct { + TagName *string `url:"tag_name,omitempty" json:"tag_name,omitempty"` + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` + Message *string `url:"message,omitempty" json:"message,omitempty"` +} + +// CreateTag creates a new tag in the repository that points to the supplied ref. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#create-a-new-tag +func (s *TagsService) CreateTag(pid interface{}, opt *CreateTagOptions, options ...OptionFunc) (*Tag, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/tags", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + t := new(Tag) + resp, err := s.client.Do(req, t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// DeleteTag deletes a tag of a repository with given name. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#delete-a-tag +func (s *TagsService) DeleteTag(pid interface{}, tag string, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/repository/tags/%s", url.QueryEscape(project), tag) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/vendor/github.com/xanzy/go-gitlab/time_stats.go b/vendor/github.com/xanzy/go-gitlab/time_stats.go new file mode 100644 index 000000000..c4a4ad40f --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/time_stats.go @@ -0,0 +1,171 @@ +package gitlab + +import ( + "fmt" + "net/url" +) + +// TimeStatsService handles communication with the time tracking related +// methods of the GitLab API. +// +// GitLab docs: https://docs.gitlab.com/ce/workflow/time_tracking.html +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html +type TimeStatsService struct { + client *Client +} + +// TimeStats represents the time estimates and time spent for an issue. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html +type TimeStats struct { + HumanTimeEstimate string `json:"human_time_estimate"` + HumanTotalTimeSpent string `json:"human_total_time_spent"` + TimeEstimate int `json:"time_estimate"` + TotalTimeSpent int `json:"total_time_spent"` +} + +func (t TimeStats) String() string { + return Stringify(t) +} + +// SetTimeEstimateOptions represents the available SetTimeEstimate() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/issues.html#set-a-time-estimate-for-an-issue +type SetTimeEstimateOptions struct { + Duration *string `url:"duration,omitempty" json:"duration,omitempty"` +} + +// SetTimeEstimate sets the time estimate for a single project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/issues.html#set-a-time-estimate-for-an-issue +func (s *TimeStatsService) SetTimeEstimate(pid interface{}, issue int, opt *SetTimeEstimateOptions, options ...OptionFunc) (*TimeStats, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/time_estimate", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + t := new(TimeStats) + resp, err := s.client.Do(req, t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// ResetTimeEstimate resets the time estimate for a single project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/issues.html#reset-the-time-estimate-for-an-issue +func (s *TimeStatsService) ResetTimeEstimate(pid interface{}, issue int, options ...OptionFunc) (*TimeStats, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/reset_time_estimate", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + t := new(TimeStats) + resp, err := s.client.Do(req, t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// AddSpentTimeOptions represents the available AddSpentTime() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/issues.html#add-spent-time-for-an-issue +type AddSpentTimeOptions struct { + Duration *string `url:"duration,omitempty" json:"duration,omitempty"` +} + +// AddSpentTime adds spent time for a single project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/issues.html#add-spent-time-for-an-issue +func (s *TimeStatsService) AddSpentTime(pid interface{}, issue int, opt *AddSpentTimeOptions, options ...OptionFunc) (*TimeStats, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/add_spent_time", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + t := new(TimeStats) + resp, err := s.client.Do(req, t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// ResetSpentTime resets the spent time for a single project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/issues.html#reset-spent-time-for-an-issue +func (s *TimeStatsService) ResetSpentTime(pid interface{}, issue int, options ...OptionFunc) (*TimeStats, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/reset_spent_time", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + t := new(TimeStats) + resp, err := s.client.Do(req, t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// GetTimeSpent gets the spent time for a single project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/issues.html#get-time-tracking-stats +func (s *TimeStatsService) GetTimeSpent(pid interface{}, issue int, options ...OptionFunc) (*TimeStats, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/time_stats", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + t := new(TimeStats) + resp, err := s.client.Do(req, t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} diff --git a/vendor/github.com/xanzy/go-gitlab/users.go b/vendor/github.com/xanzy/go-gitlab/users.go new file mode 100644 index 000000000..ba955a82f --- /dev/null +++ b/vendor/github.com/xanzy/go-gitlab/users.go @@ -0,0 +1,584 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "errors" + "fmt" + "time" +) + +// UsersService handles communication with the user related methods of +// the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html +type UsersService struct { + client *Client +} + +// User represents a GitLab user. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html +type User struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + Bio string `json:"bio"` + Skype string `json:"skype"` + Linkedin string `json:"linkedin"` + Twitter string `json:"twitter"` + WebsiteURL string `json:"website_url"` + ExternUID string `json:"extern_uid"` + Provider string `json:"provider"` + ThemeID int `json:"theme_id"` + ColorSchemeID int `json:"color_scheme_id"` + IsAdmin bool `json:"is_admin"` + AvatarURL string `json:"avatar_url"` + CanCreateGroup bool `json:"can_create_group"` + CanCreateProject bool `json:"can_create_project"` + ProjectsLimit int `json:"projects_limit"` + CurrentSignInAt *time.Time `json:"current_sign_in_at"` + LastSignInAt *time.Time `json:"last_sign_in_at"` + TwoFactorEnabled bool `json:"two_factor_enabled"` + Identities []*UserIdentity `json:"identities"` +} + +// UserIdentity represents a user identity +type UserIdentity struct { + Provider string `json:"provider"` + ExternUID string `json:"extern_uid"` +} + +// ListUsersOptions represents the available ListUsers() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#list-users +type ListUsersOptions struct { + ListOptions + Active *bool `url:"active,omitempty" json:"active,omitempty"` + Search *string `url:"search,omitempty" json:"search,omitempty"` + Username *string `url:"username,omitempty" json:"username,omitempty"` +} + +// ListUsers gets a list of users. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#list-users +func (s *UsersService) ListUsers(opt *ListUsersOptions, options ...OptionFunc) ([]*User, *Response, error) { + req, err := s.client.NewRequest("GET", "users", opt, options) + if err != nil { + return nil, nil, err + } + + var usr []*User + resp, err := s.client.Do(req, &usr) + if err != nil { + return nil, resp, err + } + + return usr, resp, err +} + +// GetUser gets a single user. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#single-user +func (s *UsersService) GetUser(user int, options ...OptionFunc) (*User, *Response, error) { + u := fmt.Sprintf("users/%d", user) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + usr := new(User) + resp, err := s.client.Do(req, usr) + if err != nil { + return nil, resp, err + } + + return usr, resp, err +} + +// CreateUserOptions represents the available CreateUser() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#user-creation +type CreateUserOptions struct { + Email *string `url:"email,omitempty" json:"email,omitempty"` + Password *string `url:"password,omitempty" json:"password,omitempty"` + Username *string `url:"username,omitempty" json:"username,omitempty"` + Name *string `url:"name,omitempty" json:"name,omitempty"` + Skype *string `url:"skype,omitempty" json:"skype,omitempty"` + Linkedin *string `url:"linkedin,omitempty" json:"linkedin,omitempty"` + Twitter *string `url:"twitter,omitempty" json:"twitter,omitempty"` + WebsiteURL *string `url:"website_url,omitempty" json:"website_url,omitempty"` + ProjectsLimit *int `url:"projects_limit,omitempty" json:"projects_limit,omitempty"` + ExternUID *string `url:"extern_uid,omitempty" json:"extern_uid,omitempty"` + Provider *string `url:"provider,omitempty" json:"provider,omitempty"` + Bio *string `url:"bio,omitempty" json:"bio,omitempty"` + Admin *bool `url:"admin,omitempty" json:"admin,omitempty"` + CanCreateGroup *bool `url:"can_create_group,omitempty" json:"can_create_group,omitempty"` + Confirm *bool `url:"confirm,omitempty" json:"confirm,omitempty"` +} + +// CreateUser creates a new user. Note only administrators can create new users. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#user-creation +func (s *UsersService) CreateUser(opt *CreateUserOptions, options ...OptionFunc) (*User, *Response, error) { + req, err := s.client.NewRequest("POST", "users", opt, options) + if err != nil { + return nil, nil, err + } + + usr := new(User) + resp, err := s.client.Do(req, usr) + if err != nil { + return nil, resp, err + } + + return usr, resp, err +} + +// ModifyUserOptions represents the available ModifyUser() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#user-modification +type ModifyUserOptions struct { + Email *string `url:"email,omitempty" json:"email,omitempty"` + Password *string `url:"password,omitempty" json:"password,omitempty"` + Username *string `url:"username,omitempty" json:"username,omitempty"` + Name *string `url:"name,omitempty" json:"name,omitempty"` + Skype *string `url:"skype,omitempty" json:"skype,omitempty"` + Linkedin *string `url:"linkedin,omitempty" json:"linkedin,omitempty"` + Twitter *string `url:"twitter,omitempty" json:"twitter,omitempty"` + WebsiteURL *string `url:"website_url,omitempty" json:"website_url,omitempty"` + ProjectsLimit *int `url:"projects_limit,omitempty" json:"projects_limit,omitempty"` + ExternUID *string `url:"extern_uid,omitempty" json:"extern_uid,omitempty"` + Provider *string `url:"provider,omitempty" json:"provider,omitempty"` + Bio *string `url:"bio,omitempty" json:"bio,omitempty"` + Admin *bool `url:"admin,omitempty" json:"admin,omitempty"` + CanCreateGroup *bool `url:"can_create_group,omitempty" json:"can_create_group,omitempty"` +} + +// ModifyUser modifies an existing user. Only administrators can change attributes +// of a user. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#user-modification +func (s *UsersService) ModifyUser(user int, opt *ModifyUserOptions, options ...OptionFunc) (*User, *Response, error) { + u := fmt.Sprintf("users/%d", user) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + usr := new(User) + resp, err := s.client.Do(req, usr) + if err != nil { + return nil, resp, err + } + + return usr, resp, err +} + +// DeleteUser deletes a user. Available only for administrators. This is an +// idempotent function, calling this function for a non-existent user id still +// returns a status code 200 OK. The JSON response differs if the user was +// actually deleted or not. In the former the user is returned and in the +// latter not. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#user-deletion +func (s *UsersService) DeleteUser(user int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("users/%d", user) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// CurrentUser gets currently authenticated user. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#current-user +func (s *UsersService) CurrentUser(options ...OptionFunc) (*User, *Response, error) { + req, err := s.client.NewRequest("GET", "user", nil, options) + if err != nil { + return nil, nil, err + } + + usr := new(User) + resp, err := s.client.Do(req, usr) + if err != nil { + return nil, resp, err + } + + return usr, resp, err +} + +// SSHKey represents a SSH key. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#list-ssh-keys +type SSHKey struct { + ID int `json:"id"` + Title string `json:"title"` + Key string `json:"key"` + CreatedAt *time.Time `json:"created_at"` +} + +// ListSSHKeys gets a list of currently authenticated user's SSH keys. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#list-ssh-keys +func (s *UsersService) ListSSHKeys(options ...OptionFunc) ([]*SSHKey, *Response, error) { + req, err := s.client.NewRequest("GET", "user/keys", nil, options) + if err != nil { + return nil, nil, err + } + + var k []*SSHKey + resp, err := s.client.Do(req, &k) + if err != nil { + return nil, resp, err + } + + return k, resp, err +} + +// ListSSHKeysForUser gets a list of a specified user's SSH keys. Available +// only for admin +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#list-ssh-keys-for-user +func (s *UsersService) ListSSHKeysForUser(user int, options ...OptionFunc) ([]*SSHKey, *Response, error) { + u := fmt.Sprintf("users/%d/keys", user) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var k []*SSHKey + resp, err := s.client.Do(req, &k) + if err != nil { + return nil, resp, err + } + + return k, resp, err +} + +// GetSSHKey gets a single key. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#single-ssh-key +func (s *UsersService) GetSSHKey(kid int, options ...OptionFunc) (*SSHKey, *Response, error) { + u := fmt.Sprintf("user/keys/%d", kid) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + k := new(SSHKey) + resp, err := s.client.Do(req, k) + if err != nil { + return nil, resp, err + } + + return k, resp, err +} + +// AddSSHKeyOptions represents the available AddSSHKey() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#add-ssh-key +type AddSSHKeyOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + Key *string `url:"key,omitempty" json:"key,omitempty"` +} + +// AddSSHKey creates a new key owned by the currently authenticated user. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#add-ssh-key +func (s *UsersService) AddSSHKey(opt *AddSSHKeyOptions, options ...OptionFunc) (*SSHKey, *Response, error) { + req, err := s.client.NewRequest("POST", "user/keys", opt, options) + if err != nil { + return nil, nil, err + } + + k := new(SSHKey) + resp, err := s.client.Do(req, k) + if err != nil { + return nil, resp, err + } + + return k, resp, err +} + +// AddSSHKeyForUser creates new key owned by specified user. Available only for +// admin. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#add-ssh-key-for-user +func (s *UsersService) AddSSHKeyForUser(user int, opt *AddSSHKeyOptions, options ...OptionFunc) (*SSHKey, *Response, error) { + u := fmt.Sprintf("users/%d/keys", user) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + k := new(SSHKey) + resp, err := s.client.Do(req, k) + if err != nil { + return nil, resp, err + } + + return k, resp, err +} + +// DeleteSSHKey deletes key owned by currently authenticated user. This is an +// idempotent function and calling it on a key that is already deleted or not +// available results in 200 OK. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#delete-ssh-key-for-current-owner +func (s *UsersService) DeleteSSHKey(kid int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("user/keys/%d", kid) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DeleteSSHKeyForUser deletes key owned by a specified user. Available only +// for admin. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#delete-ssh-key-for-given-user +func (s *UsersService) DeleteSSHKeyForUser(user int, kid int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("users/%d/keys/%d", user, kid) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// BlockUser blocks the specified user. Available only for admin. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#block-user +func (s *UsersService) BlockUser(user int, options ...OptionFunc) error { + u := fmt.Sprintf("users/%d/block", user) + + req, err := s.client.NewRequest("PUT", u, nil, options) + if err != nil { + return err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + return err + } + + switch resp.StatusCode { + case 200: + return nil + case 403: + return errors.New("Cannot block a user that is already blocked by LDAP synchronization") + case 404: + return errors.New("User does not exists") + default: + return fmt.Errorf("Received unexpected result code: %d", resp.StatusCode) + } +} + +// UnblockUser unblocks the specified user. Available only for admin. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#unblock-user +func (s *UsersService) UnblockUser(user int, options ...OptionFunc) error { + u := fmt.Sprintf("users/%d/unblock", user) + + req, err := s.client.NewRequest("PUT", u, nil, options) + if err != nil { + return err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + return err + } + + switch resp.StatusCode { + case 200: + return nil + case 403: + return errors.New("Cannot unblock a user that is blocked by LDAP synchronization") + case 404: + return errors.New("User does not exists") + default: + return fmt.Errorf("Received unexpected result code: %d", resp.StatusCode) + } +} + +// Email represents an Email. +// +// GitLab API docs: https://doc.gitlab.com/ce/api/users.html#list-emails +type Email struct { + ID int `json:"id"` + Email string `json:"email"` +} + +// ListEmails gets a list of currently authenticated user's Emails. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#list-emails +func (s *UsersService) ListEmails(options ...OptionFunc) ([]*Email, *Response, error) { + req, err := s.client.NewRequest("GET", "user/emails", nil, options) + if err != nil { + return nil, nil, err + } + + var e []*Email + resp, err := s.client.Do(req, &e) + if err != nil { + return nil, resp, err + } + + return e, resp, err +} + +// ListEmailsForUser gets a list of a specified user's Emails. Available +// only for admin +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#list-emails-for-user +func (s *UsersService) ListEmailsForUser(uid int, options ...OptionFunc) ([]*Email, *Response, error) { + u := fmt.Sprintf("users/%d/emails", uid) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var e []*Email + resp, err := s.client.Do(req, &e) + if err != nil { + return nil, resp, err + } + + return e, resp, err +} + +// GetEmail gets a single email. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#single-email +func (s *UsersService) GetEmail(eid int, options ...OptionFunc) (*Email, *Response, error) { + u := fmt.Sprintf("user/emails/%d", eid) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + e := new(Email) + resp, err := s.client.Do(req, e) + if err != nil { + return nil, resp, err + } + + return e, resp, err +} + +// AddEmailOptions represents the available AddEmail() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#add-email +type AddEmailOptions struct { + Email *string `url:"email,omitempty" json:"email,omitempty"` +} + +// AddEmail creates a new email owned by the currently authenticated user. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#add-email +func (s *UsersService) AddEmail(opt *AddEmailOptions, options ...OptionFunc) (*Email, *Response, error) { + req, err := s.client.NewRequest("POST", "user/emails", opt, options) + if err != nil { + return nil, nil, err + } + + e := new(Email) + resp, err := s.client.Do(req, e) + if err != nil { + return nil, resp, err + } + + return e, resp, err +} + +// AddEmailForUser creates new email owned by specified user. Available only for +// admin. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#add-email-for-user +func (s *UsersService) AddEmailForUser(uid int, opt *AddEmailOptions, options ...OptionFunc) (*Email, *Response, error) { + u := fmt.Sprintf("users/%d/emails", uid) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + e := new(Email) + resp, err := s.client.Do(req, e) + if err != nil { + return nil, resp, err + } + + return e, resp, err +} + +// DeleteEmail deletes email owned by currently authenticated user. This is an +// idempotent function and calling it on a key that is already deleted or not +// available results in 200 OK. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#delete-email-for-current-owner +func (s *UsersService) DeleteEmail(eid int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("user/emails/%d", eid) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DeleteEmailForUser deletes email owned by a specified user. Available only +// for admin. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#delete-email-for-given-user +func (s *UsersService) DeleteEmailForUser(uid int, eid int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("users/%d/emails/%d", uid, eid) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index e5c2a1fc9..2733edbf4 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -3036,6 +3036,12 @@ "revision": "deca6e17c056012b540aa2d2eae3ea1e63203b85", "revisionTime": "2016-10-26T18:16:49Z" }, + { + "checksumSHA1": "1mR4/KWIQEL8rzyDlCR4CRrTqGQ=", + "path": "github.com/xanzy/go-gitlab", + "revision": "5b756e2fdc9f21fd4791fa1453b8fe01af0f82e2", + "revisionTime": "2017-03-22T12:21:15Z" + }, { "checksumSHA1": "iHiMTBffQvWYlOLu3130JXuQpgQ=", "path": "github.com/xanzy/ssh-agent", From fd98f1e0c54d28879f85dd5d1d8fe75f3e3557dd Mon Sep 17 00:00:00 2001 From: Bernerd Schaefer Date: Fri, 21 Apr 2017 14:22:42 -0700 Subject: [PATCH 05/89] providers/heroku: create app in space --- .../providers/heroku/resource_heroku_app.go | 17 +++++ .../heroku/resource_heroku_app_test.go | 62 ++++++++++++++++++- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/builtin/providers/heroku/resource_heroku_app.go b/builtin/providers/heroku/resource_heroku_app.go index 20a6c9c0d..18b0dc668 100644 --- a/builtin/providers/heroku/resource_heroku_app.go +++ b/builtin/providers/heroku/resource_heroku_app.go @@ -16,6 +16,7 @@ import ( type herokuApplication struct { Name string Region string + Space string Stack string GitURL string WebURL string @@ -62,6 +63,9 @@ func (a *application) Update() error { a.App.Stack = app.Stack.Name a.App.GitURL = app.GitURL a.App.WebURL = app.WebURL + if app.Space != nil { + a.App.Space = app.Space.Name + } if app.Organization != nil { a.App.OrganizationName = app.Organization.Name } else { @@ -96,6 +100,12 @@ func resourceHerokuApp() *schema.Resource { Required: true, }, + "space": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "region": { Type: schema.TypeString, Required: true, @@ -257,6 +267,11 @@ func resourceHerokuOrgAppCreate(d *schema.ResourceData, meta interface{}) error log.Printf("[DEBUG] App region: %s", vs) opts.Region = &vs } + if v, ok := d.GetOk("space"); ok { + vs := v.(string) + log.Printf("[DEBUG] App space: %s", vs) + opts.Space = &vs + } if v, ok := d.GetOk("stack"); ok { vs := v.(string) log.Printf("[DEBUG] App stack: %s", vs) @@ -320,6 +335,8 @@ func resourceHerokuAppRead(d *schema.ResourceData, meta interface{}) error { d.Set("config_vars", configVarsValue) d.Set("all_config_vars", app.Vars) if organizationApp { + d.Set("space", app.App.Space) + orgDetails := map[string]interface{}{ "name": app.App.OrganizationName, "locked": app.App.Locked, diff --git a/builtin/providers/heroku/resource_heroku_app_test.go b/builtin/providers/heroku/resource_heroku_app_test.go index caeade8f2..e807cd83f 100644 --- a/builtin/providers/heroku/resource_heroku_app_test.go +++ b/builtin/providers/heroku/resource_heroku_app_test.go @@ -128,7 +128,37 @@ func TestAccHerokuApp_Organization(t *testing.T) { Config: testAccCheckHerokuAppConfig_organization(appName, org), Check: resource.ComposeTestCheckFunc( testAccCheckHerokuAppExistsOrg("heroku_app.foobar", &app), - testAccCheckHerokuAppAttributesOrg(&app, appName, org), + testAccCheckHerokuAppAttributesOrg(&app, appName, "", org), + ), + }, + }, + }) +} + +func TestAccHerokuApp_Space(t *testing.T) { + var app heroku.OrganizationApp + appName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) + org := os.Getenv("HEROKU_ORGANIZATION") + space := os.Getenv("HEROKU_SPACE") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + if org == "" { + t.Skip("HEROKU_ORGANIZATION is not set; skipping test.") + } + if space == "" { + t.Skip("HEROKU_SPACE is not set; skipping test.") + } + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckHerokuAppConfig_space(appName, space, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExistsOrg("heroku_app.foobar", &app), + testAccCheckHerokuAppAttributesOrg(&app, appName, space, org), ), }, }, @@ -230,14 +260,23 @@ func testAccCheckHerokuAppAttributesNoVars(app *heroku.AppInfoResult, appName st } } -func testAccCheckHerokuAppAttributesOrg(app *heroku.OrganizationApp, appName string, org string) resource.TestCheckFunc { +func testAccCheckHerokuAppAttributesOrg(app *heroku.OrganizationApp, appName, space, org string) resource.TestCheckFunc { return func(s *terraform.State) error { client := testAccProvider.Meta().(*heroku.Service) - if app.Region.Name != "us" { + if app.Region.Name != "us" && app.Region.Name != "virginia" { return fmt.Errorf("Bad region: %s", app.Region.Name) } + var appSpace string + if app.Space != nil { + appSpace = app.Space.Name + } + + if appSpace != space { + return fmt.Errorf("Bad space: %s", appSpace) + } + if app.Stack.Name != "cedar-14" { return fmt.Errorf("Bad stack: %s", app.Stack.Name) } @@ -371,3 +410,20 @@ resource "heroku_app" "foobar" { } }`, appName, org) } + +func testAccCheckHerokuAppConfig_space(appName, space, org string) string { + return fmt.Sprintf(` +resource "heroku_app" "foobar" { + name = "%s" + space = "%s" + region = "virginia" + + organization { + name = "%s" + } + + config_vars { + FOO = "bar" + } +}`, appName, space, org) +} From 7dd598baacfb9f136259858454584ba6b337c20e Mon Sep 17 00:00:00 2001 From: Bernerd Schaefer Date: Fri, 21 Apr 2017 14:28:35 -0700 Subject: [PATCH 06/89] Document `space` --- website/source/docs/providers/heroku/r/app.html.markdown | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/source/docs/providers/heroku/r/app.html.markdown b/website/source/docs/providers/heroku/r/app.html.markdown index 410f01ead..c2776e531 100644 --- a/website/source/docs/providers/heroku/r/app.html.markdown +++ b/website/source/docs/providers/heroku/r/app.html.markdown @@ -39,6 +39,7 @@ The following arguments are supported: variables, but rather variables you want present. That is, other configuration variables set externally won't be removed by Terraform if they aren't present in this list. +* `space` - (Optional) The name of a private space to create the app in. * `organization` - (Optional) A block that can be specified once to define organization settings for this app. The fields for this block are documented below. @@ -58,6 +59,7 @@ The following attributes are exported: unique ID. * `stack` - The application stack is what platform to run the application in. +* `space` - The private space the app should run in. * `region` - The region that the app should be deployed in. * `git_url` - The Git URL for the application. This is used for deploying new versions of the app. From a217acf789489a2ed0d40f769af62c559a118591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Sun, 23 Apr 2017 20:32:04 +0200 Subject: [PATCH 07/89] ignition: internal cache moved to global, instead per provider instance --- builtin/providers/ignition/provider.go | 27 ++++++++++--------- .../ignition/resource_ignition_config.go | 4 +-- .../ignition/resource_ignition_disk.go | 4 +-- .../ignition/resource_ignition_file.go | 4 +-- .../ignition/resource_ignition_filesystem.go | 4 +-- .../ignition/resource_ignition_group.go | 4 +-- .../resource_ignition_networkd_unit.go | 4 +-- .../ignition/resource_ignition_raid.go | 4 +-- .../resource_ignition_systemd_unit.go | 4 +-- .../ignition/resource_ignition_user.go | 4 +-- 10 files changed, 33 insertions(+), 30 deletions(-) diff --git a/builtin/providers/ignition/provider.go b/builtin/providers/ignition/provider.go index 6ad832d03..81462e361 100644 --- a/builtin/providers/ignition/provider.go +++ b/builtin/providers/ignition/provider.go @@ -15,6 +15,21 @@ import ( "github.com/hashicorp/terraform/terraform" ) +// globalCache keeps the instances of the internal types of ignition generated +// by the different data resources with the goal to be reused by the +// ignition_config data resource. The key of the maps are a hash of the types +// calculated on the type serialized to JSON. +var globalCache = &cache{ + disks: make(map[string]*types.Disk, 0), + arrays: make(map[string]*types.Raid, 0), + filesystems: make(map[string]*types.Filesystem, 0), + files: make(map[string]*types.File, 0), + systemdUnits: make(map[string]*types.SystemdUnit, 0), + networkdUnits: make(map[string]*types.NetworkdUnit, 0), + users: make(map[string]*types.User, 0), + groups: make(map[string]*types.Group, 0), +} + func Provider() terraform.ResourceProvider { return &schema.Provider{ DataSourcesMap: map[string]*schema.Resource{ @@ -66,18 +81,6 @@ func Provider() terraform.ResourceProvider { resourceGroup(), ), }, - ConfigureFunc: func(*schema.ResourceData) (interface{}, error) { - return &cache{ - disks: make(map[string]*types.Disk, 0), - arrays: make(map[string]*types.Raid, 0), - filesystems: make(map[string]*types.Filesystem, 0), - files: make(map[string]*types.File, 0), - systemdUnits: make(map[string]*types.SystemdUnit, 0), - networkdUnits: make(map[string]*types.NetworkdUnit, 0), - users: make(map[string]*types.User, 0), - groups: make(map[string]*types.Group, 0), - }, nil - }, } } diff --git a/builtin/providers/ignition/resource_ignition_config.go b/builtin/providers/ignition/resource_ignition_config.go index 4df56f79c..c75e50afd 100644 --- a/builtin/providers/ignition/resource_ignition_config.go +++ b/builtin/providers/ignition/resource_ignition_config.go @@ -91,7 +91,7 @@ func resourceConfig() *schema.Resource { } func resourceIgnitionFileRead(d *schema.ResourceData, meta interface{}) error { - rendered, err := renderConfig(d, meta.(*cache)) + rendered, err := renderConfig(d, globalCache) if err != nil { return err } @@ -105,7 +105,7 @@ func resourceIgnitionFileRead(d *schema.ResourceData, meta interface{}) error { } func resourceIgnitionFileExists(d *schema.ResourceData, meta interface{}) (bool, error) { - rendered, err := renderConfig(d, meta.(*cache)) + rendered, err := renderConfig(d, globalCache) if err != nil { return false, err } diff --git a/builtin/providers/ignition/resource_ignition_disk.go b/builtin/providers/ignition/resource_ignition_disk.go index 3fa9b947a..8ef6c7e05 100644 --- a/builtin/providers/ignition/resource_ignition_disk.go +++ b/builtin/providers/ignition/resource_ignition_disk.go @@ -59,7 +59,7 @@ func resourceDisk() *schema.Resource { } func resourceDiskRead(d *schema.ResourceData, meta interface{}) error { - id, err := buildDisk(d, meta.(*cache)) + id, err := buildDisk(d, globalCache) if err != nil { return err } @@ -69,7 +69,7 @@ func resourceDiskRead(d *schema.ResourceData, meta interface{}) error { } func resourceDiskExists(d *schema.ResourceData, meta interface{}) (bool, error) { - id, err := buildDisk(d, meta.(*cache)) + id, err := buildDisk(d, globalCache) if err != nil { return false, err } diff --git a/builtin/providers/ignition/resource_ignition_file.go b/builtin/providers/ignition/resource_ignition_file.go index a6f34f2fa..0f73ea6ed 100644 --- a/builtin/providers/ignition/resource_ignition_file.go +++ b/builtin/providers/ignition/resource_ignition_file.go @@ -90,7 +90,7 @@ func resourceFile() *schema.Resource { } func resourceFileRead(d *schema.ResourceData, meta interface{}) error { - id, err := buildFile(d, meta.(*cache)) + id, err := buildFile(d, globalCache) if err != nil { return err } @@ -100,7 +100,7 @@ func resourceFileRead(d *schema.ResourceData, meta interface{}) error { } func resourceFileExists(d *schema.ResourceData, meta interface{}) (bool, error) { - id, err := buildFile(d, meta.(*cache)) + id, err := buildFile(d, globalCache) if err != nil { return false, err } diff --git a/builtin/providers/ignition/resource_ignition_filesystem.go b/builtin/providers/ignition/resource_ignition_filesystem.go index a26c3f700..ce858e80c 100644 --- a/builtin/providers/ignition/resource_ignition_filesystem.go +++ b/builtin/providers/ignition/resource_ignition_filesystem.go @@ -63,7 +63,7 @@ func resourceFilesystem() *schema.Resource { } func resourceFilesystemRead(d *schema.ResourceData, meta interface{}) error { - id, err := buildFilesystem(d, meta.(*cache)) + id, err := buildFilesystem(d, globalCache) if err != nil { return err } @@ -73,7 +73,7 @@ func resourceFilesystemRead(d *schema.ResourceData, meta interface{}) error { } func resourceFilesystemExists(d *schema.ResourceData, meta interface{}) (bool, error) { - id, err := buildFilesystem(d, meta.(*cache)) + id, err := buildFilesystem(d, globalCache) if err != nil { return false, err } diff --git a/builtin/providers/ignition/resource_ignition_group.go b/builtin/providers/ignition/resource_ignition_group.go index 29ebeecd9..125e97e73 100644 --- a/builtin/providers/ignition/resource_ignition_group.go +++ b/builtin/providers/ignition/resource_ignition_group.go @@ -30,7 +30,7 @@ func resourceGroup() *schema.Resource { } func resourceGroupRead(d *schema.ResourceData, meta interface{}) error { - id, err := buildGroup(d, meta.(*cache)) + id, err := buildGroup(d, globalCache) if err != nil { return err } @@ -40,7 +40,7 @@ func resourceGroupRead(d *schema.ResourceData, meta interface{}) error { } func resourceGroupExists(d *schema.ResourceData, meta interface{}) (bool, error) { - id, err := buildGroup(d, meta.(*cache)) + id, err := buildGroup(d, globalCache) if err != nil { return false, err } diff --git a/builtin/providers/ignition/resource_ignition_networkd_unit.go b/builtin/providers/ignition/resource_ignition_networkd_unit.go index e5d822570..9fd40ed51 100644 --- a/builtin/providers/ignition/resource_ignition_networkd_unit.go +++ b/builtin/providers/ignition/resource_ignition_networkd_unit.go @@ -25,7 +25,7 @@ func resourceNetworkdUnit() *schema.Resource { } func resourceNetworkdUnitRead(d *schema.ResourceData, meta interface{}) error { - id, err := buildNetworkdUnit(d, meta.(*cache)) + id, err := buildNetworkdUnit(d, globalCache) if err != nil { return err } @@ -40,7 +40,7 @@ func resourceNetworkdUnitDelete(d *schema.ResourceData, meta interface{}) error } func resourceNetworkdUnitExists(d *schema.ResourceData, meta interface{}) (bool, error) { - id, err := buildNetworkdUnit(d, meta.(*cache)) + id, err := buildNetworkdUnit(d, globalCache) if err != nil { return false, err } diff --git a/builtin/providers/ignition/resource_ignition_raid.go b/builtin/providers/ignition/resource_ignition_raid.go index 1c8b732ed..dab1a5f7c 100644 --- a/builtin/providers/ignition/resource_ignition_raid.go +++ b/builtin/providers/ignition/resource_ignition_raid.go @@ -36,7 +36,7 @@ func resourceRaid() *schema.Resource { } func resourceRaidRead(d *schema.ResourceData, meta interface{}) error { - id, err := buildRaid(d, meta.(*cache)) + id, err := buildRaid(d, globalCache) if err != nil { return err } @@ -46,7 +46,7 @@ func resourceRaidRead(d *schema.ResourceData, meta interface{}) error { } func resourceRaidExists(d *schema.ResourceData, meta interface{}) (bool, error) { - id, err := buildRaid(d, meta.(*cache)) + id, err := buildRaid(d, globalCache) if err != nil { return false, err } diff --git a/builtin/providers/ignition/resource_ignition_systemd_unit.go b/builtin/providers/ignition/resource_ignition_systemd_unit.go index 211380024..88fe9b206 100644 --- a/builtin/providers/ignition/resource_ignition_systemd_unit.go +++ b/builtin/providers/ignition/resource_ignition_systemd_unit.go @@ -55,7 +55,7 @@ func resourceSystemdUnit() *schema.Resource { } func resourceSystemdUnitRead(d *schema.ResourceData, meta interface{}) error { - id, err := buildSystemdUnit(d, meta.(*cache)) + id, err := buildSystemdUnit(d, globalCache) if err != nil { return err } @@ -65,7 +65,7 @@ func resourceSystemdUnitRead(d *schema.ResourceData, meta interface{}) error { } func resourceSystemdUnitExists(d *schema.ResourceData, meta interface{}) (bool, error) { - id, err := buildSystemdUnit(d, meta.(*cache)) + id, err := buildSystemdUnit(d, globalCache) if err != nil { return false, err } diff --git a/builtin/providers/ignition/resource_ignition_user.go b/builtin/providers/ignition/resource_ignition_user.go index dd4ccc153..183e6c8c1 100644 --- a/builtin/providers/ignition/resource_ignition_user.go +++ b/builtin/providers/ignition/resource_ignition_user.go @@ -79,7 +79,7 @@ func resourceUser() *schema.Resource { } func resourceUserRead(d *schema.ResourceData, meta interface{}) error { - id, err := buildUser(d, meta.(*cache)) + id, err := buildUser(d, globalCache) if err != nil { return err } @@ -89,7 +89,7 @@ func resourceUserRead(d *schema.ResourceData, meta interface{}) error { } func resourceUserExists(d *schema.ResourceData, meta interface{}) (bool, error) { - id, err := buildUser(d, meta.(*cache)) + id, err := buildUser(d, globalCache) if err != nil { return false, err } From 631b0b865cfd35bfeb1abe793aaf8131617d05fb Mon Sep 17 00:00:00 2001 From: Richard Clamp Date: Wed, 19 Oct 2016 15:25:12 +0100 Subject: [PATCH 08/89] provider/gitlab: add gitlab provider and `gitlab_project` resource Here we add a basic provider with a single resource type. It's copied heavily from the `github` provider and `github_repository` resource, as there is some overlap in those types/apis. ~~~ resource "gitlab_project" "test1" { name = "test1" visibility_level = "public" } ~~~ We implement in terms of the [go-gitlab](https://github.com/xanzy/go-gitlab) library, which provides a wrapping of the [gitlab api](https://docs.gitlab.com/ee/api/) We have been a little selective in the properties we surface for the project resource, as not all properties are very instructive. Notable is the removal of the `public` bool as the `visibility_level` will take precedent if both are supplied which leads to confusing interactions if they disagree. --- builtin/bins/provider-gitlab/main.go | 12 + builtin/providers/gitlab/config.go | 31 +++ builtin/providers/gitlab/provider.go | 52 +++++ builtin/providers/gitlab/provider_test.go | 35 +++ .../gitlab/resource_gitlab_project.go | 207 ++++++++++++++++++ .../gitlab/resource_gitlab_project_test.go | 185 ++++++++++++++++ builtin/providers/gitlab/util.go | 54 +++++ builtin/providers/gitlab/util_test.go | 65 ++++++ command/internal_plugin_list.go | 2 + .../docs/providers/gitlab/index.html.markdown | 41 ++++ .../providers/gitlab/r/project.html.markdown | 58 +++++ 11 files changed, 742 insertions(+) create mode 100644 builtin/bins/provider-gitlab/main.go create mode 100644 builtin/providers/gitlab/config.go create mode 100644 builtin/providers/gitlab/provider.go create mode 100644 builtin/providers/gitlab/provider_test.go create mode 100644 builtin/providers/gitlab/resource_gitlab_project.go create mode 100644 builtin/providers/gitlab/resource_gitlab_project_test.go create mode 100644 builtin/providers/gitlab/util.go create mode 100644 builtin/providers/gitlab/util_test.go create mode 100644 website/source/docs/providers/gitlab/index.html.markdown create mode 100644 website/source/docs/providers/gitlab/r/project.html.markdown diff --git a/builtin/bins/provider-gitlab/main.go b/builtin/bins/provider-gitlab/main.go new file mode 100644 index 000000000..acb94705d --- /dev/null +++ b/builtin/bins/provider-gitlab/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/gitlab" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: gitlab.Provider, + }) +} diff --git a/builtin/providers/gitlab/config.go b/builtin/providers/gitlab/config.go new file mode 100644 index 000000000..288f7ba6a --- /dev/null +++ b/builtin/providers/gitlab/config.go @@ -0,0 +1,31 @@ +package gitlab + +import ( + "github.com/xanzy/go-gitlab" +) + +// Config is per-provider, specifies where to connect to gitlab +type Config struct { + Token string + BaseURL string +} + +// Client returns a *gitlab.Client to interact with the configured gitlab instance +func (c *Config) Client() (interface{}, error) { + client := gitlab.NewClient(nil, c.Token) + if c.BaseURL != "" { + err := client.SetBaseURL(c.BaseURL) + if err != nil { + // The BaseURL supplied wasn't valid, bail. + return nil, err + } + } + + // Test the credentials by checking we can get information about the authenticated user. + _, _, err := client.Users.CurrentUser() + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/builtin/providers/gitlab/provider.go b/builtin/providers/gitlab/provider.go new file mode 100644 index 000000000..f4389d694 --- /dev/null +++ b/builtin/providers/gitlab/provider.go @@ -0,0 +1,52 @@ +package gitlab + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +// Provider returns a terraform.ResourceProvider. +func Provider() terraform.ResourceProvider { + + // The actual provider + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "token": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("GITLAB_TOKEN", nil), + Description: descriptions["token"], + }, + "base_url": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("GITLAB_BASE_URL", ""), + Description: descriptions["base_url"], + }, + }, + ResourcesMap: map[string]*schema.Resource{ + "gitlab_project": resourceGitlabProject(), + }, + + ConfigureFunc: providerConfigure, + } +} + +var descriptions map[string]string + +func init() { + descriptions = map[string]string{ + "token": "The OAuth token used to connect to GitLab.", + + "base_url": "The GitLab Base API URL", + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + config := Config{ + Token: d.Get("token").(string), + BaseURL: d.Get("base_url").(string), + } + + return config.Client() +} diff --git a/builtin/providers/gitlab/provider_test.go b/builtin/providers/gitlab/provider_test.go new file mode 100644 index 000000000..a28eddb8d --- /dev/null +++ b/builtin/providers/gitlab/provider_test.go @@ -0,0 +1,35 @@ +package gitlab + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "gitlab": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("GITLAB_TOKEN"); v == "" { + t.Fatal("GITLAB_TOKEN must be set for acceptance tests") + } +} diff --git a/builtin/providers/gitlab/resource_gitlab_project.go b/builtin/providers/gitlab/resource_gitlab_project.go new file mode 100644 index 000000000..b4824f36a --- /dev/null +++ b/builtin/providers/gitlab/resource_gitlab_project.go @@ -0,0 +1,207 @@ +package gitlab + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + gitlab "github.com/xanzy/go-gitlab" +) + +func resourceGitlabProject() *schema.Resource { + return &schema.Resource{ + Create: resourceGitlabProjectCreate, + Read: resourceGitlabProjectRead, + Update: resourceGitlabProjectUpdate, + Delete: resourceGitlabProjectDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "default_branch": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "issues_enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "merge_requests_enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "wiki_enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "snippets_enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "visibility_level": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateValueFunc([]string{"private", "internal", "public"}), + Default: "private", + }, + + "ssh_url_to_repo": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "http_url_to_repo": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "web_url": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceGitlabProjectUpdateFromAPI(d *schema.ResourceData, project *gitlab.Project) { + d.Set("name", project.Name) + d.Set("description", project.Description) + d.Set("default_branch", project.DefaultBranch) + d.Set("issues_enabled", project.IssuesEnabled) + d.Set("merge_requests_enabled", project.MergeRequestsEnabled) + d.Set("wiki_enabled", project.WikiEnabled) + d.Set("snippets_enabled", project.SnippetsEnabled) + d.Set("visibility_level", visibilityLevelToString(project.VisibilityLevel)) + + d.Set("ssh_url_to_repo", project.SSHURLToRepo) + d.Set("http_url_to_repo", project.HTTPURLToRepo) + d.Set("web_url", project.WebURL) +} + +func resourceGitlabProjectCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*gitlab.Client) + options := &gitlab.CreateProjectOptions{ + Name: gitlab.String(d.Get("name").(string)), + } + + if v, ok := d.GetOk("description"); ok { + options.Description = gitlab.String(v.(string)) + } + + if v, ok := d.GetOk("issues_enabled"); ok { + options.IssuesEnabled = gitlab.Bool(v.(bool)) + } + + if v, ok := d.GetOk("merge_requests_enabled"); ok { + options.MergeRequestsEnabled = gitlab.Bool(v.(bool)) + } + + if v, ok := d.GetOk("wiki_enabled"); ok { + options.WikiEnabled = gitlab.Bool(v.(bool)) + } + + if v, ok := d.GetOk("snippets_enabled"); ok { + options.SnippetsEnabled = gitlab.Bool(v.(bool)) + } + + if v, ok := d.GetOk("visibility_level"); ok { + options.VisibilityLevel = stringToVisibilityLevel(v.(string)) + } + + log.Printf("[DEBUG] create gitlab project %q", options.Name) + + project, _, err := client.Projects.CreateProject(options) + if err != nil { + return err + } + + d.SetId(fmt.Sprintf("%d", project.ID)) + + resourceGitlabProjectUpdateFromAPI(d, project) + + return nil +} + +func resourceGitlabProjectRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*gitlab.Client) + log.Printf("[DEBUG] read gitlab project %s", d.Id()) + + project, response, err := client.Projects.GetProject(d.Id()) + if err != nil { + if response.StatusCode == 404 { + log.Printf("[WARN] removing project %s from state because it no longer exists in gitlab", d.Id()) + d.SetId("") + return nil + } + + return err + } + + resourceGitlabProjectUpdateFromAPI(d, project) + return nil +} + +func resourceGitlabProjectUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*gitlab.Client) + + options := &gitlab.EditProjectOptions{} + + if d.HasChange("name") { + options.Name = gitlab.String(d.Get("name").(string)) + } + + if d.HasChange("description") { + options.Description = gitlab.String(d.Get("description").(string)) + } + + if d.HasChange("default_branch") { + options.DefaultBranch = gitlab.String(d.Get("description").(string)) + } + + if d.HasChange("visibility_level") { + options.VisibilityLevel = stringToVisibilityLevel(d.Get("visibility_level").(string)) + } + + if d.HasChange("issues_enabled") { + options.IssuesEnabled = gitlab.Bool(d.Get("issues_enabled").(bool)) + } + + if d.HasChange("merge_requests_enabled") { + options.MergeRequestsEnabled = gitlab.Bool(d.Get("merge_requests_enabled").(bool)) + } + + if d.HasChange("wiki_enabled") { + options.WikiEnabled = gitlab.Bool(d.Get("wiki_enabled").(bool)) + } + + if d.HasChange("snippets_enabled") { + options.SnippetsEnabled = gitlab.Bool(d.Get("snippets_enabled").(bool)) + } + + log.Printf("[DEBUG] update gitlab project %s", d.Id()) + + project, _, err := client.Projects.EditProject(d.Id(), options) + if err != nil { + return err + } + + resourceGitlabProjectUpdateFromAPI(d, project) + + return nil +} + +func resourceGitlabProjectDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*gitlab.Client) + log.Printf("[DEBUG] update gitlab project %s", d.Id()) + + _, err := client.Projects.DeleteProject(d.Id()) + return err +} diff --git a/builtin/providers/gitlab/resource_gitlab_project_test.go b/builtin/providers/gitlab/resource_gitlab_project_test.go new file mode 100644 index 000000000..d4b53e476 --- /dev/null +++ b/builtin/providers/gitlab/resource_gitlab_project_test.go @@ -0,0 +1,185 @@ +package gitlab + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-gitlab" +) + +func TestAccGitlabProject_basic(t *testing.T) { + var project gitlab.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGitlabProjectDestroy, + Steps: []resource.TestStep{ + // Create a project with all the features on + resource.TestStep{ + Config: testAccGitlabProjectConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckGitlabProjectExists("gitlab_project.foo", &project), + testAccCheckGitlabProjectAttributes(&project, &testAccGitlabProjectExpectedAttributes{ + Name: "foo", + Description: "Terraform acceptance tests", + IssuesEnabled: true, + MergeRequestsEnabled: true, + WikiEnabled: true, + SnippetsEnabled: true, + VisibilityLevel: 20, + }), + ), + }, + // Update the project to turn the features off + resource.TestStep{ + Config: testAccGitlabProjectUpdateConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckGitlabProjectExists("gitlab_project.foo", &project), + testAccCheckGitlabProjectAttributes(&project, &testAccGitlabProjectExpectedAttributes{ + Name: "foo", + Description: "Terraform acceptance tests!", + VisibilityLevel: 20, + }), + ), + }, + // Update the project to turn the features on again + resource.TestStep{ + Config: testAccGitlabProjectConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckGitlabProjectExists("gitlab_project.foo", &project), + testAccCheckGitlabProjectAttributes(&project, &testAccGitlabProjectExpectedAttributes{ + Name: "foo", + Description: "Terraform acceptance tests", + IssuesEnabled: true, + MergeRequestsEnabled: true, + WikiEnabled: true, + SnippetsEnabled: true, + VisibilityLevel: 20, + }), + ), + }, + }, + }) +} + +func testAccCheckGitlabProjectExists(n string, project *gitlab.Project) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not Found: %s", n) + } + + repoName := rs.Primary.ID + if repoName == "" { + return fmt.Errorf("No project ID is set") + } + conn := testAccProvider.Meta().(*gitlab.Client) + + gotProject, _, err := conn.Projects.GetProject(repoName) + if err != nil { + return err + } + *project = *gotProject + return nil + } +} + +type testAccGitlabProjectExpectedAttributes struct { + Name string + Description string + DefaultBranch string + IssuesEnabled bool + MergeRequestsEnabled bool + WikiEnabled bool + SnippetsEnabled bool + VisibilityLevel gitlab.VisibilityLevelValue +} + +func testAccCheckGitlabProjectAttributes(project *gitlab.Project, want *testAccGitlabProjectExpectedAttributes) resource.TestCheckFunc { + return func(s *terraform.State) error { + if project.Name != want.Name { + return fmt.Errorf("got repo %q; want %q", project.Name, want.Name) + } + if project.Description != want.Description { + return fmt.Errorf("got description %q; want %q", project.Description, want.Description) + } + + if project.DefaultBranch != want.DefaultBranch { + return fmt.Errorf("got default_branch %q; want %q", project.DefaultBranch, want.DefaultBranch) + } + + if project.IssuesEnabled != want.IssuesEnabled { + return fmt.Errorf("got issues_enabled %t; want %t", project.IssuesEnabled, want.IssuesEnabled) + } + + if project.MergeRequestsEnabled != want.MergeRequestsEnabled { + return fmt.Errorf("got merge_requests_enabled %t; want %t", project.MergeRequestsEnabled, want.MergeRequestsEnabled) + } + + if project.WikiEnabled != want.WikiEnabled { + return fmt.Errorf("got wiki_enabled %t; want %t", project.WikiEnabled, want.WikiEnabled) + } + + if project.SnippetsEnabled != want.SnippetsEnabled { + return fmt.Errorf("got snippets_enabled %t; want %t", project.SnippetsEnabled, want.SnippetsEnabled) + } + + if project.VisibilityLevel != want.VisibilityLevel { + return fmt.Errorf("got default branch %q; want %q", project.VisibilityLevel, want.VisibilityLevel) + } + + return nil + } +} + +func testAccCheckGitlabProjectDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*gitlab.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "gitlab_project" { + continue + } + + gotRepo, resp, err := conn.Projects.GetProject(rs.Primary.ID) + if err == nil { + if gotRepo != nil && fmt.Sprintf("%d", gotRepo.ID) == rs.Primary.ID { + return fmt.Errorf("Repository still exists") + } + } + if resp.StatusCode != 404 { + return err + } + return nil + } + return nil +} + +const testAccGitlabProjectConfig = ` +resource "gitlab_project" "foo" { + name = "foo" + description = "Terraform acceptance tests" + + # So that acceptance tests can be run in a gitlab organization + # with no billing + visibility_level = "public" +} +` + +const testAccGitlabProjectUpdateConfig = ` +resource "gitlab_project" "foo" { + name = "foo" + description = "Terraform acceptance tests!" + + # So that acceptance tests can be run in a gitlab organization + # with no billing + visibility_level = "public" + + issues_enabled = false + merge_requests_enabled = false + wiki_enabled = false + snippets_enabled = false +} +` diff --git a/builtin/providers/gitlab/util.go b/builtin/providers/gitlab/util.go new file mode 100644 index 000000000..942e30852 --- /dev/null +++ b/builtin/providers/gitlab/util.go @@ -0,0 +1,54 @@ +package gitlab + +import ( + "fmt" + + "github.com/hashicorp/terraform/helper/schema" + gitlab "github.com/xanzy/go-gitlab" +) + +// copied from ../github/util.go +func validateValueFunc(values []string) schema.SchemaValidateFunc { + return func(v interface{}, k string) (we []string, errors []error) { + value := v.(string) + valid := false + for _, role := range values { + if value == role { + valid = true + break + } + } + + if !valid { + errors = append(errors, fmt.Errorf("%s is an invalid value for argument %s", value, k)) + } + return + } +} + +func stringToVisibilityLevel(s string) *gitlab.VisibilityLevelValue { + lookup := map[string]gitlab.VisibilityLevelValue{ + "private": gitlab.PrivateVisibility, + "internal": gitlab.InternalVisibility, + "public": gitlab.PublicVisibility, + } + + value, ok := lookup[s] + if !ok { + return nil + } + return &value +} + +func visibilityLevelToString(v gitlab.VisibilityLevelValue) *string { + lookup := map[gitlab.VisibilityLevelValue]string{ + gitlab.PrivateVisibility: "private", + gitlab.InternalVisibility: "internal", + gitlab.PublicVisibility: "public", + } + value, ok := lookup[v] + if !ok { + return nil + } + return &value +} diff --git a/builtin/providers/gitlab/util_test.go b/builtin/providers/gitlab/util_test.go new file mode 100644 index 000000000..465eec73c --- /dev/null +++ b/builtin/providers/gitlab/util_test.go @@ -0,0 +1,65 @@ +package gitlab + +import ( + "testing" + + "github.com/xanzy/go-gitlab" +) + +func TestGitlab_validation(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + { + Value: "invalid", + ErrCount: 1, + }, + { + Value: "valid_one", + ErrCount: 0, + }, + { + Value: "valid_two", + ErrCount: 0, + }, + } + + validationFunc := validateValueFunc([]string{"valid_one", "valid_two"}) + + for _, tc := range cases { + _, errors := validationFunc(tc.Value, "test_arg") + + if len(errors) != tc.ErrCount { + t.Fatalf("Expected 1 validation error") + } + } +} + +func TestGitlab_visbilityHelpers(t *testing.T) { + cases := []struct { + String string + Level gitlab.VisibilityLevelValue + }{ + { + String: "private", + Level: gitlab.PrivateVisibility, + }, + { + String: "public", + Level: gitlab.PublicVisibility, + }, + } + + for _, tc := range cases { + level := stringToVisibilityLevel(tc.String) + if level == nil || *level != tc.Level { + t.Fatalf("got %v expected %v", level, tc.Level) + } + + sv := visibilityLevelToString(tc.Level) + if sv == nil || *sv != tc.String { + t.Fatalf("got %v expected %v", sv, tc.String) + } + } +} diff --git a/command/internal_plugin_list.go b/command/internal_plugin_list.go index 2f48908c7..cfcf5e37c 100644 --- a/command/internal_plugin_list.go +++ b/command/internal_plugin_list.go @@ -31,6 +31,7 @@ import ( externalprovider "github.com/hashicorp/terraform/builtin/providers/external" fastlyprovider "github.com/hashicorp/terraform/builtin/providers/fastly" githubprovider "github.com/hashicorp/terraform/builtin/providers/github" + gitlabprovider "github.com/hashicorp/terraform/builtin/providers/gitlab" googleprovider "github.com/hashicorp/terraform/builtin/providers/google" grafanaprovider "github.com/hashicorp/terraform/builtin/providers/grafana" herokuprovider "github.com/hashicorp/terraform/builtin/providers/heroku" @@ -107,6 +108,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{ "external": externalprovider.Provider, "fastly": fastlyprovider.Provider, "github": githubprovider.Provider, + "gitlab": gitlabprovider.Provider, "google": googleprovider.Provider, "grafana": grafanaprovider.Provider, "heroku": herokuprovider.Provider, diff --git a/website/source/docs/providers/gitlab/index.html.markdown b/website/source/docs/providers/gitlab/index.html.markdown new file mode 100644 index 000000000..cf6fd431a --- /dev/null +++ b/website/source/docs/providers/gitlab/index.html.markdown @@ -0,0 +1,41 @@ +--- +layout: "gitlab" +page_title: "Provider: GitLab" +sidebar_current: "docs-gitlab-index" +description: |- + The GitLab provider is used to interact with GitLab organization resources. +--- + +# GitLab Provider + +The GitLab provider is used to interact with GitLab organization resources. + +The provider allows you to manage your GitLab organization's members and teams easily. +It needs to be configured with the proper credentials before it can be used. + +Use the navigation to the left to read about the available resources. + +## Example Usage + +``` +# Configure the GitLab Provider +provider "gitlab" { + token = "${var.github_token}" +} + +# Add a project to the organization +resource "gitlab_project" "sample_project" { + ... +} +``` + +## Argument Reference + +The following arguments are supported in the `provider` block: + +* `token` - (Optional) This is the GitLab personal access token. It must be provided, but + it can also be sourced from the `GITLAB_TOKEN` environment variable. + +* `base_url` - (Optional) This is the target GitLab base API endpoint. Providing a value is a + requirement when working with GitLab CE or GitLab Enterprise. It is optional to provide this value and + it can also be sourced from the `GITLAB_BASE_URL` environment variable. The value must end with a slash. diff --git a/website/source/docs/providers/gitlab/r/project.html.markdown b/website/source/docs/providers/gitlab/r/project.html.markdown new file mode 100644 index 000000000..4d807c960 --- /dev/null +++ b/website/source/docs/providers/gitlab/r/project.html.markdown @@ -0,0 +1,58 @@ +--- +layout: "gitlab" +page_title: "GitLab: gitlab_project" +sidebar_current: "docs-gitlab-resource-project" +description: |- + Creates and manages projects within Github organizations +--- + +# gitlab\_project + +This resource allows you to create and manage projects within your +GitLab organization. + + +## Example Usage + +``` +resource "gitlab_repository" "example" { + name = "example" + description = "My awesome codebase" + + visbility_level = "public" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the project. + +* `description` - (Optional) A description of the project. + +* `default_branch` - (Optional) The default branch for the project. + +* `issues_enabled` - (Optional) Enable issue tracking for the project. + +* `merge_requests_enabled` - (Optional) Enable merge requests for the project. + +* `wiki_enabled` - (Optional) Enable wiki for the project. + +* `snippets_enabled` - (Optional) Enable snippets for the project. + +* `visbility_level` - (Optional) Set to `public` to create a public project. + Valid values are `private`, `internal`, `public`. + Repositories are created as private by default. + +## Attributes Reference + +The following additional attributes are exported: + +* `ssh_url_to_repo` - URL that can be provided to `git clone` to clone the + repository via SSH. + +* `http_url_to_repo` - URL that can be provided to `git clone` to clone the + repository via HTTP. + +* `web_url` - URL that can be used to find the project in a browser. From acdb5c659a86773ecb32c24c7108aaacd543de08 Mon Sep 17 00:00:00 2001 From: Bernerd Schaefer Date: Mon, 24 Apr 2017 11:29:28 -0700 Subject: [PATCH 09/89] provider/heroku: set app buildpacks from config Many apps deployed to Heroku require that multiple buildpacks be configured in a particular order to operate correctly. This updates the builtin Heroku provider's app resource to support configuring buildpacks and the related documentation in the website. Similar to config vars, externally set buildpacks will not be altered if the config is not set. --- .../providers/heroku/resource_heroku_app.go | 67 +++++++ .../heroku/resource_heroku_app_test.go | 164 ++++++++++++++++++ .../docs/providers/heroku/r/app.html.markdown | 6 + 3 files changed, 237 insertions(+) diff --git a/builtin/providers/heroku/resource_heroku_app.go b/builtin/providers/heroku/resource_heroku_app.go index 20a6c9c0d..da0a704a5 100644 --- a/builtin/providers/heroku/resource_heroku_app.go +++ b/builtin/providers/heroku/resource_heroku_app.go @@ -30,6 +30,7 @@ type application struct { App *herokuApplication // The heroku application Client *heroku.Service // Client to interact with the heroku API Vars map[string]string // The vars on the application + Buildpacks []string // The application's buildpack names or URLs Organization bool // is the application organization app } @@ -71,6 +72,11 @@ func (a *application) Update() error { } } + a.Buildpacks, err = retrieveBuildpacks(a.Id, a.Client) + if err != nil { + errs = append(errs, err) + } + a.Vars, err = retrieveConfigVars(a.Id, a.Client) if err != nil { errs = append(errs, err) @@ -109,6 +115,14 @@ func resourceHerokuApp() *schema.Resource { ForceNew: true, }, + "buildpacks": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "config_vars": { Type: schema.TypeList, Optional: true, @@ -215,6 +229,10 @@ func resourceHerokuAppCreate(d *schema.ResourceData, meta interface{}) error { } } + if v, ok := d.GetOk("buildpacks"); ok { + err = updateBuildpacks(d.Id(), client, v.([]interface{})) + } + return resourceHerokuAppRead(d, meta) } @@ -293,6 +311,9 @@ func resourceHerokuAppRead(d *schema.ResourceData, meta interface{}) error { } } + // Only track buildpacks when set in the configuration. + _, buildpacksConfigured := d.GetOk("buildpacks") + organizationApp := isOrganizationApp(d) // Only set the config_vars that we have set in the configuration. @@ -317,6 +338,9 @@ func resourceHerokuAppRead(d *schema.ResourceData, meta interface{}) error { d.Set("region", app.App.Region) d.Set("git_url", app.App.GitURL) d.Set("web_url", app.App.WebURL) + if buildpacksConfigured { + d.Set("buildpacks", app.Buildpacks) + } d.Set("config_vars", configVarsValue) d.Set("all_config_vars", app.Vars) if organizationApp { @@ -374,6 +398,13 @@ func resourceHerokuAppUpdate(d *schema.ResourceData, meta interface{}) error { } } + if d.HasChange("buildpacks") { + err := updateBuildpacks(d.Id(), client, d.Get("buildpacks").([]interface{})) + if err != nil { + return err + } + } + return resourceHerokuAppRead(d, meta) } @@ -402,6 +433,21 @@ func resourceHerokuAppRetrieve(id string, organization bool, client *heroku.Serv return &app, nil } +func retrieveBuildpacks(id string, client *heroku.Service) ([]string, error) { + results, err := client.BuildpackInstallationList(context.TODO(), id, nil) + + if err != nil { + return nil, err + } + + buildpacks := []string{} + for _, installation := range results { + buildpacks = append(buildpacks, installation.Buildpack.Name) + } + + return buildpacks, nil +} + func retrieveConfigVars(id string, client *heroku.Service) (map[string]string, error) { vars, err := client.ConfigVarInfoForApp(context.TODO(), id) @@ -450,3 +496,24 @@ func updateConfigVars( return nil } + +func updateBuildpacks(id string, client *heroku.Service, v []interface{}) error { + opts := heroku.BuildpackInstallationUpdateOpts{ + Updates: []struct { + Buildpack string `json:"buildpack" url:"buildpack,key"` + }{}} + + for _, buildpack := range v { + opts.Updates = append(opts.Updates, struct { + Buildpack string `json:"buildpack" url:"buildpack,key"` + }{ + Buildpack: buildpack.(string), + }) + } + + if _, err := client.BuildpackInstallationUpdate(context.TODO(), id, opts); err != nil { + return fmt.Errorf("Error updating buildpacks: %s", err) + } + + return nil +} diff --git a/builtin/providers/heroku/resource_heroku_app_test.go b/builtin/providers/heroku/resource_heroku_app_test.go index caeade8f2..21d2a379b 100644 --- a/builtin/providers/heroku/resource_heroku_app_test.go +++ b/builtin/providers/heroku/resource_heroku_app_test.go @@ -109,6 +109,75 @@ func TestAccHerokuApp_NukeVars(t *testing.T) { }) } +func TestAccHerokuApp_Buildpacks(t *testing.T) { + var app heroku.AppInfoResult + appName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckHerokuAppConfig_go(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppBuildpacks(appName, false), + resource.TestCheckResourceAttr("heroku_app.foobar", "buildpacks.0", "heroku/go"), + ), + }, + { + Config: testAccCheckHerokuAppConfig_multi(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppBuildpacks(appName, true), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "buildpacks.0", "https://github.com/heroku/heroku-buildpack-multi-procfile"), + resource.TestCheckResourceAttr("heroku_app.foobar", "buildpacks.1", "heroku/go"), + ), + }, + { + Config: testAccCheckHerokuAppConfig_no_vars(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppNoBuildpacks(appName), + resource.TestCheckNoResourceAttr("heroku_app.foobar", "buildpacks.0"), + ), + }, + }, + }) +} + +func TestAccHerokuApp_ExternallySetBuildpacks(t *testing.T) { + var app heroku.AppInfoResult + appName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckHerokuAppConfig_no_vars(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppNoBuildpacks(appName), + resource.TestCheckNoResourceAttr("heroku_app.foobar", "buildpacks.0"), + ), + }, + { + PreConfig: testAccInstallUnconfiguredBuildpack(t, appName), + Config: testAccCheckHerokuAppConfig_no_vars(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppBuildpacks(appName, false), + resource.TestCheckNoResourceAttr("heroku_app.foobar", "buildpacks.0"), + ), + }, + }, + }) +} + func TestAccHerokuApp_Organization(t *testing.T) { var app heroku.OrganizationApp appName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) @@ -230,6 +299,59 @@ func testAccCheckHerokuAppAttributesNoVars(app *heroku.AppInfoResult, appName st } } +func testAccCheckHerokuAppBuildpacks(appName string, multi bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*heroku.Service) + + results, err := client.BuildpackInstallationList(context.TODO(), appName, nil) + if err != nil { + return err + } + + buildpacks := []string{} + for _, installation := range results { + buildpacks = append(buildpacks, installation.Buildpack.Name) + } + + if multi { + herokuMulti := "https://github.com/heroku/heroku-buildpack-multi-procfile" + if len(buildpacks) != 2 || buildpacks[0] != herokuMulti || buildpacks[1] != "heroku/go" { + return fmt.Errorf("Bad buildpacks: %v", buildpacks) + } + + return nil + } + + if len(buildpacks) != 1 || buildpacks[0] != "heroku/go" { + return fmt.Errorf("Bad buildpacks: %v", buildpacks) + } + + return nil + } +} + +func testAccCheckHerokuAppNoBuildpacks(appName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*heroku.Service) + + results, err := client.BuildpackInstallationList(context.TODO(), appName, nil) + if err != nil { + return err + } + + buildpacks := []string{} + for _, installation := range results { + buildpacks = append(buildpacks, installation.Buildpack.Name) + } + + if len(buildpacks) != 0 { + return fmt.Errorf("Bad buildpacks: %v", buildpacks) + } + + return nil + } +} + func testAccCheckHerokuAppAttributesOrg(app *heroku.OrganizationApp, appName string, org string) resource.TestCheckFunc { return func(s *terraform.State) error { client := testAccProvider.Meta().(*heroku.Service) @@ -323,6 +445,25 @@ func testAccCheckHerokuAppExistsOrg(n string, app *heroku.OrganizationApp) resou } } +func testAccInstallUnconfiguredBuildpack(t *testing.T, appName string) func() { + return func() { + client := testAccProvider.Meta().(*heroku.Service) + + opts := heroku.BuildpackInstallationUpdateOpts{ + Updates: []struct { + Buildpack string `json:"buildpack" url:"buildpack,key"` + }{ + {Buildpack: "heroku/go"}, + }, + } + + _, err := client.BuildpackInstallationUpdate(context.TODO(), appName, opts) + if err != nil { + t.Fatalf("Error updating buildpacks: %s", err) + } + } +} + func testAccCheckHerokuAppConfig_basic(appName string) string { return fmt.Sprintf(` resource "heroku_app" "foobar" { @@ -335,6 +476,29 @@ resource "heroku_app" "foobar" { }`, appName) } +func testAccCheckHerokuAppConfig_go(appName string) string { + return fmt.Sprintf(` +resource "heroku_app" "foobar" { + name = "%s" + region = "us" + + buildpacks = ["heroku/go"] +}`, appName) +} + +func testAccCheckHerokuAppConfig_multi(appName string) string { + return fmt.Sprintf(` +resource "heroku_app" "foobar" { + name = "%s" + region = "us" + + buildpacks = [ + "https://github.com/heroku/heroku-buildpack-multi-procfile", + "heroku/go" + ] +}`, appName) +} + func testAccCheckHerokuAppConfig_updated(appName string) string { return fmt.Sprintf(` resource "heroku_app" "foobar" { diff --git a/website/source/docs/providers/heroku/r/app.html.markdown b/website/source/docs/providers/heroku/r/app.html.markdown index 410f01ead..9e6b42963 100644 --- a/website/source/docs/providers/heroku/r/app.html.markdown +++ b/website/source/docs/providers/heroku/r/app.html.markdown @@ -22,6 +22,10 @@ resource "heroku_app" "default" { config_vars { FOOBAR = "baz" } + + buildpacks = [ + "heroku/go" + ] } ``` @@ -34,6 +38,8 @@ The following arguments are supported: * `region` - (Required) The region that the app should be deployed in. * `stack` - (Optional) The application stack is what platform to run the application in. +* `buildpacks` - (Optional) Buildpack names or URLs for the application. + Buildpacks configured externally won't be altered if this is not present. * `config_vars` - (Optional) Configuration variables for the application. The config variables in this map are not the final set of configuration variables, but rather variables you want present. That is, other From 15aabe93c3a31ab75a33930985c249492c8e0a96 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 24 Apr 2017 15:23:52 -0600 Subject: [PATCH 10/89] Randomize mongodb names --- .../aws/resource_aws_ecs_service_test.go | 135 ++++++++++-------- 1 file changed, 76 insertions(+), 59 deletions(-) diff --git a/builtin/providers/aws/resource_aws_ecs_service_test.go b/builtin/providers/aws/resource_aws_ecs_service_test.go index f622d64b7..c3a603547 100644 --- a/builtin/providers/aws/resource_aws_ecs_service_test.go +++ b/builtin/providers/aws/resource_aws_ecs_service_test.go @@ -85,20 +85,21 @@ func TestParseTaskDefinition(t *testing.T) { } func TestAccAWSEcsServiceWithARN(t *testing.T) { + rInt := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckAWSEcsServiceDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSEcsService, + Config: testAccAWSEcsService(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"), ), }, { - Config: testAccAWSEcsServiceModified, + Config: testAccAWSEcsServiceModified(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"), ), @@ -181,13 +182,14 @@ func TestAccAWSEcsService_withIamRole(t *testing.T) { } func TestAccAWSEcsService_withDeploymentValues(t *testing.T) { + rInt := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckAWSEcsServiceDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSEcsServiceWithDeploymentValues, + Config: testAccAWSEcsServiceWithDeploymentValues(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"), resource.TestCheckResourceAttr( @@ -262,20 +264,21 @@ func TestAccAWSEcsService_withAlb(t *testing.T) { } func TestAccAWSEcsServiceWithPlacementStrategy(t *testing.T) { + rInt := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckAWSEcsServiceDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSEcsService, + Config: testAccAWSEcsService(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"), resource.TestCheckResourceAttr("aws_ecs_service.mongo", "placement_strategy.#", "0"), ), }, { - Config: testAccAWSEcsServiceWithPlacementStrategy, + Config: testAccAWSEcsServiceWithPlacementStrategy(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"), resource.TestCheckResourceAttr("aws_ecs_service.mongo", "placement_strategy.#", "1"), @@ -286,13 +289,14 @@ func TestAccAWSEcsServiceWithPlacementStrategy(t *testing.T) { } func TestAccAWSEcsServiceWithPlacementConstraints(t *testing.T) { + rInt := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckAWSEcsServiceDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSEcsServiceWithPlacementConstraint, + Config: testAccAWSEcsServiceWithPlacementConstraint(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"), resource.TestCheckResourceAttr("aws_ecs_service.mongo", "placement_constraints.#", "1"), @@ -303,13 +307,14 @@ func TestAccAWSEcsServiceWithPlacementConstraints(t *testing.T) { } func TestAccAWSEcsServiceWithPlacementConstraints_emptyExpression(t *testing.T) { + rInt := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckAWSEcsServiceDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSEcsServiceWithPlacementConstraintEmptyExpression, + Config: testAccAWSEcsServiceWithPlacementConstraintEmptyExpression(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"), resource.TestCheckResourceAttr("aws_ecs_service.mongo", "placement_constraints.#", "1"), @@ -366,9 +371,10 @@ func testAccCheckAWSEcsServiceExists(name string) resource.TestCheckFunc { } } -var testAccAWSEcsService = ` +func testAccAWSEcsService(rInt int) string { + return fmt.Sprintf(` resource "aws_ecs_cluster" "default" { - name = "terraformecstest1" + name = "terraformecstest%d" } resource "aws_ecs_task_definition" "mongo" { @@ -387,16 +393,18 @@ DEFINITION } resource "aws_ecs_service" "mongo" { - name = "mongodb" + name = "mongodb-%d" cluster = "${aws_ecs_cluster.default.id}" task_definition = "${aws_ecs_task_definition.mongo.arn}" desired_count = 1 } -` +`, rInt, rInt) +} -var testAccAWSEcsServiceModified = ` +func testAccAWSEcsServiceModified(rInt int) string { + return fmt.Sprintf(` resource "aws_ecs_cluster" "default" { - name = "terraformecstest1" + name = "terraformecstest%d" } resource "aws_ecs_task_definition" "mongo" { @@ -415,16 +423,18 @@ DEFINITION } resource "aws_ecs_service" "mongo" { - name = "mongodb" + name = "mongodb-%d" cluster = "${aws_ecs_cluster.default.id}" task_definition = "${aws_ecs_task_definition.mongo.arn}" desired_count = 2 } -` +`, rInt, rInt) +} -var testAccAWSEcsServiceWithPlacementStrategy = ` +func testAccAWSEcsServiceWithPlacementStrategy(rInt int) string { + return fmt.Sprintf(` resource "aws_ecs_cluster" "default" { - name = "terraformecstest1" + name = "terraformecstest%d" } resource "aws_ecs_task_definition" "mongo" { @@ -443,7 +453,7 @@ DEFINITION } resource "aws_ecs_service" "mongo" { - name = "mongodb" + name = "mongodb-%d" cluster = "${aws_ecs_cluster.default.id}" task_definition = "${aws_ecs_task_definition.mongo.arn}" desired_count = 1 @@ -452,43 +462,47 @@ resource "aws_ecs_service" "mongo" { field = "memory" } } -` +`, rInt, rInt) +} -var testAccAWSEcsServiceWithPlacementConstraint = ` +func testAccAWSEcsServiceWithPlacementConstraint(rInt int) string { + return fmt.Sprintf(` + resource "aws_ecs_cluster" "default" { + name = "terraformecstest%d" + } + + resource "aws_ecs_task_definition" "mongo" { + family = "mongodb" + container_definitions = < Date: Mon, 24 Apr 2017 18:06:28 -0400 Subject: [PATCH 11/89] provider/aws: Add `network_interface` to instance --- .../providers/aws/resource_aws_instance.go | 225 ++++++++++++------ .../aws/resource_aws_instance_test.go | 129 +++++++++- .../providers/aws/r/instance.html.markdown | 55 +++++ 3 files changed, 337 insertions(+), 72 deletions(-) diff --git a/builtin/providers/aws/resource_aws_instance.go b/builtin/providers/aws/resource_aws_instance.go index e5d4a128f..481aa4cd1 100644 --- a/builtin/providers/aws/resource_aws_instance.go +++ b/builtin/providers/aws/resource_aws_instance.go @@ -128,18 +128,43 @@ func resourceAwsInstance() *schema.Resource { Computed: true, }, - // TODO: Deprecate me + // TODO: Deprecate me v0.10.0 "network_interface_id": { + Type: schema.TypeString, + Computed: true, + Deprecated: "Please use `primary_network_interface_id` instead", + }, + + "primary_network_interface_id": { Type: schema.TypeString, Computed: true, }, - "primary_network_interface": { - ConflictsWith: []string{"associate_public_ip_address", "subnet_id", "private_ip", "vpc_security_group_ids", "security_groups", "ipv6_addresses", "ipv6_address_count"}, - Type: schema.TypeString, + "network_interface": { + ConflictsWith: []string{"associate_public_ip_address", "subnet_id", "private_ip", "vpc_security_group_ids", "security_groups", "ipv6_addresses", "ipv6_address_count", "source_dest_check"}, + Type: schema.TypeSet, Optional: true, - ForceNew: true, Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delete_on_termination": { + Type: schema.TypeBool, + Default: false, + Optional: true, + ForceNew: true, + }, + "network_interface_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "device_index": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + }, + }, }, "public_ip": { @@ -537,25 +562,62 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { d.Set("private_ip", instance.PrivateIpAddress) d.Set("iam_instance_profile", iamInstanceProfileArnToName(instance.IamInstanceProfile)) + // Set configured Network Interface Device Index Slice + // We only want to read, and populate state for the configured network_interface attachments. Otherwise, other + // resources have the potential to attach network interfaces to the instance, and cause a perpetual create/destroy + // diff. We should only read on changes configured for this specific resource because of this. + var configuredDeviceIndexes []int + if v, ok := d.GetOk("network_interface"); ok { + vL := v.(*schema.Set).List() + for _, vi := range vL { + mVi := vi.(map[string]interface{}) + configuredDeviceIndexes = append(configuredDeviceIndexes, mVi["device_index"].(int)) + } + } + var ipv6Addresses []string if len(instance.NetworkInterfaces) > 0 { - for _, ni := range instance.NetworkInterfaces { - if *ni.Attachment.DeviceIndex == 0 { - d.Set("subnet_id", ni.SubnetId) - d.Set("network_interface_id", ni.NetworkInterfaceId) // TODO: Deprecate me - d.Set("primary_network_interface", ni.NetworkInterfaceId) - d.Set("associate_public_ip_address", ni.Association != nil) - d.Set("ipv6_address_count", len(ni.Ipv6Addresses)) - - for _, address := range ni.Ipv6Addresses { - ipv6Addresses = append(ipv6Addresses, *address.Ipv6Address) + var primaryNetworkInterface ec2.InstanceNetworkInterface + var networkInterfaces []map[string]interface{} + for _, iNi := range instance.NetworkInterfaces { + ni := make(map[string]interface{}) + if *iNi.Attachment.DeviceIndex == 0 { + primaryNetworkInterface = *iNi + } + // If the attached network device is inside our configuration, refresh state with values found. + // Otherwise, assume the network device was attached via an outside resource. + for _, index := range configuredDeviceIndexes { + if index == int(*iNi.Attachment.DeviceIndex) { + ni["device_index"] = *iNi.Attachment.DeviceIndex + ni["network_interface_id"] = *iNi.NetworkInterfaceId + ni["delete_on_termination"] = *iNi.Attachment.DeleteOnTermination } } + // Don't add empty network interfaces to schema + if len(ni) == 0 { + continue + } + networkInterfaces = append(networkInterfaces, ni) } + if err := d.Set("network_interface", networkInterfaces); err != nil { + return fmt.Errorf("Error setting network_interfaces: %v", err) + } + + // Set primary network interface details + d.Set("subnet_id", primaryNetworkInterface.SubnetId) + d.Set("network_interface_id", primaryNetworkInterface.NetworkInterfaceId) // TODO: Deprecate me v0.10.0 + d.Set("primary_network_interface_id", primaryNetworkInterface.NetworkInterfaceId) + d.Set("associate_public_ip_address", primaryNetworkInterface.Association != nil) + d.Set("ipv6_address_count", len(primaryNetworkInterface.Ipv6Addresses)) + + for _, address := range primaryNetworkInterface.Ipv6Addresses { + ipv6Addresses = append(ipv6Addresses, *address.Ipv6Address) + } + } else { d.Set("subnet_id", instance.SubnetId) - d.Set("network_interface_id", "") // TODO: Deprecate me - d.Set("primary_network_interface", "") + d.Set("network_interface_id", "") // TODO: Deprecate me v0.10.0 + d.Set("primary_network_interface_id", "") } if err := d.Set("ipv6_addresses", ipv6Addresses); err != nil { @@ -682,24 +744,28 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error { } } - if d.HasChange("source_dest_check") || d.IsNewResource() { - // SourceDestCheck can only be set on VPC instances // AWS will return an error of InvalidParameterCombination if we attempt - // to modify the source_dest_check of an instance in EC2 Classic - log.Printf("[INFO] Modifying `source_dest_check` on Instance %s", d.Id()) - _, err := conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeInput{ - InstanceId: aws.String(d.Id()), - SourceDestCheck: &ec2.AttributeBooleanValue{ - Value: aws.Bool(d.Get("source_dest_check").(bool)), - }, - }) - if err != nil { - if ec2err, ok := err.(awserr.Error); ok { - // Toloerate InvalidParameterCombination error in Classic, otherwise - // return the error - if "InvalidParameterCombination" != ec2err.Code() { - return err + // SourceDestCheck can only be modified on an instance without manually specified network interfaces. + // SourceDestCheck, in that case, is configured at the network interface level + if _, ok := d.GetOk("network_interface"); !ok { + if d.HasChange("source_dest_check") || d.IsNewResource() { + // SourceDestCheck can only be set on VPC instances // AWS will return an error of InvalidParameterCombination if we attempt + // to modify the source_dest_check of an instance in EC2 Classic + log.Printf("[INFO] Modifying `source_dest_check` on Instance %s", d.Id()) + _, err := conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeInput{ + InstanceId: aws.String(d.Id()), + SourceDestCheck: &ec2.AttributeBooleanValue{ + Value: aws.Bool(d.Get("source_dest_check").(bool)), + }, + }) + if err != nil { + if ec2err, ok := err.(awserr.Error); ok { + // Tolerate InvalidParameterCombination error in Classic, otherwise + // return the error + if "InvalidParameterCombination" != ec2err.Code() { + return err + } + log.Printf("[WARN] Attempted to modify SourceDestCheck on non VPC instance: %s", ec2err.Message()) } - log.Printf("[WARN] Attempted to modify SourceDestCheck on non VPC instance: %s", ec2err.Message()) } } } @@ -1019,6 +1085,55 @@ func fetchRootDeviceName(ami string, conn *ec2.EC2) (*string, error) { return rootDeviceName, nil } +func buildNetworkInterfaceOpts(d *schema.ResourceData, groups []*string, nInterfaces interface{}) []*ec2.InstanceNetworkInterfaceSpecification { + networkInterfaces := []*ec2.InstanceNetworkInterfaceSpecification{} + // Get necessary items + associatePublicIPAddress := d.Get("associate_public_ip_address").(bool) + subnet, hasSubnet := d.GetOk("subnet_id") + + if hasSubnet && associatePublicIPAddress { + // If we have a non-default VPC / Subnet specified, we can flag + // AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided. + // You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise + // you get: Network interfaces and an instance-level subnet ID may not be specified on the same request + // You also need to attach Security Groups to the NetworkInterface instead of the instance, + // to avoid: Network interfaces and an instance-level security groups may not be specified on + // the same request + ni := &ec2.InstanceNetworkInterfaceSpecification{ + AssociatePublicIpAddress: aws.Bool(associatePublicIPAddress), + DeviceIndex: aws.Int64(int64(0)), + SubnetId: aws.String(subnet.(string)), + Groups: groups, + } + + if v, ok := d.GetOk("private_ip"); ok { + ni.PrivateIpAddress = aws.String(v.(string)) + } + + if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 { + for _, v := range v.List() { + ni.Groups = append(ni.Groups, aws.String(v.(string))) + } + } + + networkInterfaces = append(networkInterfaces, ni) + } else { + // If we have manually specified network interfaces, build and attach those here. + vL := nInterfaces.(*schema.Set).List() + for _, v := range vL { + ini := v.(map[string]interface{}) + ni := &ec2.InstanceNetworkInterfaceSpecification{ + DeviceIndex: aws.Int64(int64(ini["device_index"].(int))), + NetworkInterfaceId: aws.String(ini["network_interface_id"].(string)), + DeleteOnTermination: aws.Bool(ini["delete_on_termination"].(bool)), + } + networkInterfaces = append(networkInterfaces, ni) + } + } + + return networkInterfaces +} + func readBlockDeviceMappingsFromConfig( d *schema.ResourceData, conn *ec2.EC2) ([]*ec2.BlockDeviceMapping, error) { blockDevices := make([]*ec2.BlockDeviceMapping, 0) @@ -1271,43 +1386,10 @@ func buildAwsInstanceOpts( } } - // Check if using non-defaullt primary network interface - interface_id, interfaceOk := d.GetOk("primary_network_interface") + networkInterfaces, interfacesOk := d.GetOk("network_interface") - if hasSubnet && associatePublicIPAddress { - // If we have a non-default VPC / Subnet specified, we can flag - // AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided. - // You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise - // you get: Network interfaces and an instance-level subnet ID may not be specified on the same request - // You also need to attach Security Groups to the NetworkInterface instead of the instance, - // to avoid: Network interfaces and an instance-level security groups may not be specified on - // the same request - ni := &ec2.InstanceNetworkInterfaceSpecification{ - AssociatePublicIpAddress: aws.Bool(associatePublicIPAddress), - DeviceIndex: aws.Int64(int64(0)), - SubnetId: aws.String(subnetID), - Groups: groups, - } - - if v, ok := d.GetOk("private_ip"); ok { - ni.PrivateIpAddress = aws.String(v.(string)) - } - - if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 { - for _, v := range v.List() { - ni.Groups = append(ni.Groups, aws.String(v.(string))) - } - } - - opts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni} - } else if interfaceOk { - ni := &ec2.InstanceNetworkInterfaceSpecification{ - DeviceIndex: aws.Int64(int64(0)), - NetworkInterfaceId: aws.String(interface_id.(string)), - } - - opts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni} - } else { + // If simply specifying a subnetID, privateIP, Security Groups, or VPC Security Groups, build these now + if !hasSubnet && !associatePublicIPAddress && !interfacesOk { if subnetID != "" { opts.SubnetID = aws.String(subnetID) } @@ -1327,6 +1409,9 @@ func buildAwsInstanceOpts( opts.SecurityGroupIDs = append(opts.SecurityGroupIDs, aws.String(v.(string))) } } + } else { + // Otherwise we're attaching (a) network interface(s) + opts.NetworkInterfaces = buildNetworkInterfaceOpts(d, groups, networkInterfaces) } if v, ok := d.GetOk("key_name"); ok { diff --git a/builtin/providers/aws/resource_aws_instance_test.go b/builtin/providers/aws/resource_aws_instance_test.go index 1de8ac768..0ba5f8024 100644 --- a/builtin/providers/aws/resource_aws_instance_test.go +++ b/builtin/providers/aws/resource_aws_instance_test.go @@ -891,7 +891,38 @@ func TestAccAWSInstance_primaryNetworkInterface(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckInstanceExists("aws_instance.foo", &instance), testAccCheckAWSENIExists("aws_network_interface.bar", &ini), - resource.TestCheckResourceAttrSet("aws_instance.foo", "primary_network_interface"), + resource.TestCheckResourceAttr("aws_instance.foo", "network_interface.#", "1"), + ), + }, + }, + }) +} + +func TestAccAWSInstance_addSecondaryInterface(t *testing.T) { + var before ec2.Instance + var after ec2.Instance + var iniPrimary ec2.NetworkInterface + var iniSecondary ec2.NetworkInterface + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccInstanceConfigAddSecondaryNetworkInterfaceBefore, + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists("aws_instance.foo", &before), + testAccCheckAWSENIExists("aws_network_interface.primary", &iniPrimary), + resource.TestCheckResourceAttr("aws_instance.foo", "network_interface.#", "1"), + ), + }, + { + Config: testAccInstanceConfigAddSecondaryNetworkInterfaceAfter, + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists("aws_instance.foo", &after), + testAccCheckAWSENIExists("aws_network_interface.secondary", &iniSecondary), + resource.TestCheckResourceAttr("aws_instance.foo", "network_interface.#", "1"), ), }, }, @@ -1586,6 +1617,100 @@ resource "aws_network_interface" "bar" { resource "aws_instance" "foo" { ami = "ami-22b9a343" instance_type = "t2.micro" - primary_network_interface = "${aws_network_interface.bar.id}" + network_interface { + network_interface_id = "${aws_network_interface.bar.id}" + device_index = 0 + } +} +` + +const testAccInstanceConfigAddSecondaryNetworkInterfaceBefore = ` +resource "aws_vpc" "foo" { + cidr_block = "172.16.0.0/16" + tags { + Name = "tf-instance-test" + } +} + +resource "aws_subnet" "foo" { + vpc_id = "${aws_vpc.foo.id}" + cidr_block = "172.16.10.0/24" + availability_zone = "us-west-2a" + tags { + Name = "tf-instance-test" + } +} + +resource "aws_network_interface" "primary" { + subnet_id = "${aws_subnet.foo.id}" + private_ips = ["172.16.10.100"] + tags { + Name = "primary_network_interface" + } +} + +resource "aws_network_interface" "secondary" { + subnet_id = "${aws_subnet.foo.id}" + private_ips = ["172.16.10.101"] + tags { + Name = "secondary_network_interface" + } +} + +resource "aws_instance" "foo" { + ami = "ami-22b9a343" + instance_type = "t2.micro" + network_interface { + network_interface_id = "${aws_network_interface.primary.id}" + device_index = 0 + } +} +` + +const testAccInstanceConfigAddSecondaryNetworkInterfaceAfter = ` +resource "aws_vpc" "foo" { + cidr_block = "172.16.0.0/16" + tags { + Name = "tf-instance-test" + } +} + +resource "aws_subnet" "foo" { + vpc_id = "${aws_vpc.foo.id}" + cidr_block = "172.16.10.0/24" + availability_zone = "us-west-2a" + tags { + Name = "tf-instance-test" + } +} + +resource "aws_network_interface" "primary" { + subnet_id = "${aws_subnet.foo.id}" + private_ips = ["172.16.10.100"] + tags { + Name = "primary_network_interface" + } +} + +// Attach previously created network interface, observe no state diff on instance resource +resource "aws_network_interface" "secondary" { + subnet_id = "${aws_subnet.foo.id}" + private_ips = ["172.16.10.101"] + tags { + Name = "secondary_network_interface" + } + attachment { + instance = "${aws_instance.foo.id}" + device_index = 1 + } +} + +resource "aws_instance" "foo" { + ami = "ami-22b9a343" + instance_type = "t2.micro" + network_interface { + network_interface_id = "${aws_network_interface.primary.id}" + device_index = 0 + } } ` diff --git a/website/source/docs/providers/aws/r/instance.html.markdown b/website/source/docs/providers/aws/r/instance.html.markdown index 4d8725de2..2edb0bd0f 100644 --- a/website/source/docs/providers/aws/r/instance.html.markdown +++ b/website/source/docs/providers/aws/r/instance.html.markdown @@ -86,6 +86,7 @@ instances. See [Shutdown Behavior](https://docs.aws.amazon.com/AWSEC2/latest/Use instance. See [Block Devices](#block-devices) below for details. * `ephemeral_block_device` - (Optional) Customize Ephemeral (also known as "Instance Store") volumes on the instance. See [Block Devices](#block-devices) below for details. +* `network_interface` - (Optional) Customize network interfaces to be attached at instance boot time. See [Network Interfaces](#network-interfaces) below for more details. ## Block devices @@ -149,6 +150,59 @@ resources cannot be automatically detected by Terraform. After making updates to block device configuration, resource recreation can be manually triggered by using the [`taint` command](/docs/commands/taint.html). +## Network Interfaces + +Each of the `network_interface` blocks attach a network interface to an EC2 Instance during boot time. However, because +the network interface is attached at boot-time, replacing/modifying the network interface **WILL** trigger a recreation +of the EC2 Instance. If you should need at any point to detach/modify/re-attach a network interface to the instance, use +the `aws_network_interface` or `aws_network_interface_attachment` resources instead. + +The `network_interface` configuration block _does_, however, allow users to supply their own network interface to be used +as the default network interface on an EC2 Instance, attached at `eth0`. + +Each `network_interface` block supports the following: + +* `device_index` - (Required) The integer index of the network interface attachment. Limited by instance type. +* `network_interface_id` - (Required) The ID of the network interface to attach. +* `delete_on_termination` - (Optional) Whether or not to delete the network interface on instance termination. Defaults to `false`. + +### Example + +```hcl +resource "aws_vpc" "my_vpc" { + cidr_block = "172.16.0.0/16" + tags { + Name = "tf-example" + } +} + +resource "aws_subnet" "my_subnet" { + vpc_id = "${aws_vpc.my_vpc.id}" + cidr_block = "172.16.10.0/24" + availability_zone = "us-west-2a" + tags { + Name = "tf-example" + } +} + +resource "aws_network_interface" "foo" { + subnet_id = "${aws_subnet.my_subnet.id}" + private_ips = ["172.16.10.100"] + tags { + Name = "primary_network_interface" + } +} + +resource "aws_instance" "foo" { + ami = "ami-22b9a343" // us-west-2 + instance_type = "t2.micro" + network_interface { + network_interface_id = "${aws_network_interface.foo.id}" + device_index = 0 + } +} +``` + ## Attributes Reference The following attributes are exported: @@ -161,6 +215,7 @@ The following attributes are exported: is only available if you've enabled DNS hostnames for your VPC * `public_ip` - The public IP address assigned to the instance, if applicable. **NOTE**: If you are using an [`aws_eip`](/docs/providers/aws/r/eip.html) with your instance, you should refer to the EIP's address directly and not use `public_ip`, as this field will change after the EIP is attached. * `network_interface_id` - The ID of the network interface that was created with the instance. +* `primary_network_interface_id` - The ID of the instance's primary network interface. * `private_dns` - The private DNS name assigned to the instance. Can only be used inside the Amazon EC2, and only available if you've enabled DNS hostnames for your VPC From 43257492b930013906675a0b81491b20cbb49f90 Mon Sep 17 00:00:00 2001 From: Bernerd Schaefer Date: Mon, 24 Apr 2017 17:08:13 -0700 Subject: [PATCH 12/89] Add first failing test for heroku_space --- builtin/providers/heroku/provider.go | 5 +- .../providers/heroku/resource_heroku_space.go | 31 ++++++ .../heroku/resource_heroku_space_test.go | 95 +++++++++++++++++++ 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 builtin/providers/heroku/resource_heroku_space.go create mode 100644 builtin/providers/heroku/resource_heroku_space_test.go diff --git a/builtin/providers/heroku/provider.go b/builtin/providers/heroku/provider.go index b6ea2bc98..6a8c9b986 100644 --- a/builtin/providers/heroku/provider.go +++ b/builtin/providers/heroku/provider.go @@ -25,11 +25,12 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "heroku_app": resourceHerokuApp(), "heroku_addon": resourceHerokuAddon(), + "heroku_app": resourceHerokuApp(), + "heroku_cert": resourceHerokuCert(), "heroku_domain": resourceHerokuDomain(), "heroku_drain": resourceHerokuDrain(), - "heroku_cert": resourceHerokuCert(), + "heroku_space": resourceHerokuSpace(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/heroku/resource_heroku_space.go b/builtin/providers/heroku/resource_heroku_space.go new file mode 100644 index 000000000..d11a01194 --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_space.go @@ -0,0 +1,31 @@ +package heroku + +import "github.com/hashicorp/terraform/helper/schema" + +func resourceHerokuSpace() *schema.Resource { + return &schema.Resource{ + Create: func(_ *schema.ResourceData, _ interface{}) error { return nil }, + Read: func(_ *schema.ResourceData, _ interface{}) error { return nil }, + Update: func(_ *schema.ResourceData, _ interface{}) error { return nil }, + Delete: func(_ *schema.ResourceData, _ interface{}) error { return nil }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + + "organization": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "region": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + } +} diff --git a/builtin/providers/heroku/resource_heroku_space_test.go b/builtin/providers/heroku/resource_heroku_space_test.go new file mode 100644 index 000000000..d9112f321 --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_space_test.go @@ -0,0 +1,95 @@ +package heroku + +import ( + "context" + "fmt" + "os" + "testing" + + heroku "github.com/cyberdelia/heroku-go/v3" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccHerokuSpace_Basic(t *testing.T) { + var space heroku.SpaceInfoResult + spaceName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) + org := os.Getenv("HEROKU_ORGANIZATION") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + if org == "" { + t.Skip("HEROKU_ORGANIZATION is not set; skipping test.") + } + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuSpaceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckHerokuSpaceConfig_basic(spaceName, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuSpaceExists("heroku_space.foobar", &space), + ), + }, + }, + }) +} + +func testAccCheckHerokuSpaceConfig_basic(spaceName, orgName string) string { + return fmt.Sprintf(` +resource "heroku_space" "foobar" { + name = "%s" + organization = "%s" + region = "virginia" +} +`, spaceName, orgName) +} + +func testAccCheckHerokuSpaceExists(n string, space *heroku.SpaceInfoResult) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No space name set") + } + + client := testAccProvider.Meta().(*heroku.Service) + + foundSpace, err := client.SpaceInfo(context.TODO(), rs.Primary.ID) + if err != nil { + return err + } + + if foundSpace.Name != rs.Primary.ID { + return fmt.Errorf("Space not found") + } + + *space = *foundSpace + + return nil + } +} + +func testAccCheckHerokuSpaceDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*heroku.Service) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "heroku_space" { + continue + } + + _, err := client.SpaceInfo(context.TODO(), rs.Primary.ID) + + if err == nil { + return fmt.Errorf("Space still exists") + } + } + + return nil +} From dbf82e651ed780f9fd792faa50bc63bc22dafe9a Mon Sep 17 00:00:00 2001 From: Bernerd Schaefer Date: Mon, 24 Apr 2017 17:34:34 -0700 Subject: [PATCH 13/89] Implementation --- .../providers/heroku/resource_heroku_space.go | 90 +++++++++++++++++-- .../heroku/resource_heroku_space_test.go | 21 ++++- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/builtin/providers/heroku/resource_heroku_space.go b/builtin/providers/heroku/resource_heroku_space.go index d11a01194..82e21e56d 100644 --- a/builtin/providers/heroku/resource_heroku_space.go +++ b/builtin/providers/heroku/resource_heroku_space.go @@ -1,13 +1,19 @@ package heroku -import "github.com/hashicorp/terraform/helper/schema" +import ( + "context" + "log" + + heroku "github.com/cyberdelia/heroku-go/v3" + "github.com/hashicorp/terraform/helper/schema" +) func resourceHerokuSpace() *schema.Resource { return &schema.Resource{ - Create: func(_ *schema.ResourceData, _ interface{}) error { return nil }, - Read: func(_ *schema.ResourceData, _ interface{}) error { return nil }, - Update: func(_ *schema.ResourceData, _ interface{}) error { return nil }, - Delete: func(_ *schema.ResourceData, _ interface{}) error { return nil }, + Create: resourceHerokuSpaceCreate, + Read: resourceHerokuSpaceRead, + Update: resourceHerokuSpaceUpdate, + Delete: resourceHerokuSpaceDelete, Schema: map[string]*schema.Schema{ "name": { @@ -29,3 +35,77 @@ func resourceHerokuSpace() *schema.Resource { }, } } + +func resourceHerokuSpaceCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + opts := heroku.SpaceCreateOpts{} + opts.Name = d.Get("name").(string) + opts.Organization = d.Get("organization").(string) + + if v, ok := d.GetOk("region"); ok { + vs := v.(string) + opts.Region = &vs + } + + space, err := client.SpaceCreate(context.TODO(), opts) + if err != nil { + return err + } + + d.SetId(space.ID) + log.Printf("[INFO] Space ID: %s", d.Id()) + + setSpaceAttributes(d, (*heroku.Space)(space)) + return nil +} + +func resourceHerokuSpaceRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + space, err := client.SpaceInfo(context.TODO(), d.Id()) + if err != nil { + return err + } + + setSpaceAttributes(d, (*heroku.Space)(space)) + return nil +} + +func resourceHerokuSpaceUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + if !d.HasChange("name") { + return nil + } + + name := d.Get("name").(string) + opts := heroku.SpaceUpdateOpts{Name: &name} + + space, err := client.SpaceUpdate(context.TODO(), d.Id(), opts) + if err != nil { + return err + } + + setSpaceAttributes(d, (*heroku.Space)(space)) + return nil +} + +func setSpaceAttributes(d *schema.ResourceData, space *heroku.Space) { + d.Set("name", space.Name) + d.Set("organization", space.Organization.Name) + d.Set("region", space.Region.Name) +} + +func resourceHerokuSpaceDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + log.Printf("[INFO] Deleting space: %s", d.Id()) + _, err := client.SpaceDelete(context.TODO(), d.Id()) + if err != nil { + return err + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/heroku/resource_heroku_space_test.go b/builtin/providers/heroku/resource_heroku_space_test.go index d9112f321..72a2ed0c5 100644 --- a/builtin/providers/heroku/resource_heroku_space_test.go +++ b/builtin/providers/heroku/resource_heroku_space_test.go @@ -15,6 +15,7 @@ import ( func TestAccHerokuSpace_Basic(t *testing.T) { var space heroku.SpaceInfoResult spaceName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) + spaceName2 := fmt.Sprintf("tftest-%s", acctest.RandString(10)) org := os.Getenv("HEROKU_ORGANIZATION") resource.Test(t, resource.TestCase{ @@ -31,6 +32,14 @@ func TestAccHerokuSpace_Basic(t *testing.T) { Config: testAccCheckHerokuSpaceConfig_basic(spaceName, org), Check: resource.ComposeTestCheckFunc( testAccCheckHerokuSpaceExists("heroku_space.foobar", &space), + testAccCheckHerokuSpaceAttributes(&space, spaceName), + ), + }, + { + Config: testAccCheckHerokuSpaceConfig_basic(spaceName2, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuSpaceExists("heroku_space.foobar", &space), + testAccCheckHerokuSpaceAttributes(&space, spaceName2), ), }, }, @@ -66,7 +75,7 @@ func testAccCheckHerokuSpaceExists(n string, space *heroku.SpaceInfoResult) reso return err } - if foundSpace.Name != rs.Primary.ID { + if foundSpace.ID != rs.Primary.ID { return fmt.Errorf("Space not found") } @@ -76,6 +85,16 @@ func testAccCheckHerokuSpaceExists(n string, space *heroku.SpaceInfoResult) reso } } +func testAccCheckHerokuSpaceAttributes(space *heroku.SpaceInfoResult, spaceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if space.Name != spaceName { + return fmt.Errorf("Bad name: %s", space.Name) + } + + return nil + } +} + func testAccCheckHerokuSpaceDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*heroku.Service) From de54855f6312f6372acae33f337d9b403d945410 Mon Sep 17 00:00:00 2001 From: Bernerd Schaefer Date: Mon, 24 Apr 2017 17:42:54 -0700 Subject: [PATCH 14/89] Website --- .../providers/heroku/r/space.html.markdown | 48 +++++++++++++++++++ website/source/layouts/heroku.erb | 4 ++ 2 files changed, 52 insertions(+) create mode 100644 website/source/docs/providers/heroku/r/space.html.markdown diff --git a/website/source/docs/providers/heroku/r/space.html.markdown b/website/source/docs/providers/heroku/r/space.html.markdown new file mode 100644 index 000000000..6464d9252 --- /dev/null +++ b/website/source/docs/providers/heroku/r/space.html.markdown @@ -0,0 +1,48 @@ +--- +layout: "heroku" +page_title: "Heroku: heroku_space" +sidebar_current: "docs-heroku-resource-space" +description: |- + Provides a Heroku Space resource for running apps in isolated, highly available, secure app execution environments. +--- + +# heroku\_space + +Provides a Heroku Space resource for running apps in isolated, highly available, secure app execution environments. + +## Example Usage + +```hcl +# Create a new Heroku space +resource "heroku_space" "default" { + name = "test-space" + organization = "my-company" + region = "virginia" +} + +# Create a new Heroku app in test-space +resource "heroku_app" "default" { + name = "test-app" + space = "${heroku_space.default.name}" + organization = { + name = "my-company" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the space. +* `organization` - (Required) The name of the organization which will own the space. +* `region` - (Optional) The region that the space should be created in. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the space. +* `name` - The space's name. +* `plan` - The space's organization. +* `region` - The space's region. diff --git a/website/source/layouts/heroku.erb b/website/source/layouts/heroku.erb index 99704acf5..0957af9a0 100644 --- a/website/source/layouts/heroku.erb +++ b/website/source/layouts/heroku.erb @@ -32,6 +32,10 @@ > heroku_drain + + > + heroku_space + From 81e8db56f0ec8f6e39ecefd066d3c0c6d81f0139 Mon Sep 17 00:00:00 2001 From: Bernerd Schaefer Date: Mon, 24 Apr 2017 17:50:52 -0700 Subject: [PATCH 15/89] Fix typo in docs --- website/source/docs/providers/heroku/r/space.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/providers/heroku/r/space.html.markdown b/website/source/docs/providers/heroku/r/space.html.markdown index 6464d9252..ae573029b 100644 --- a/website/source/docs/providers/heroku/r/space.html.markdown +++ b/website/source/docs/providers/heroku/r/space.html.markdown @@ -44,5 +44,5 @@ The following attributes are exported: * `id` - The ID of the space. * `name` - The space's name. -* `plan` - The space's organization. +* `organization` - The space's organization. * `region` - The space's region. From 10ee31d5a2bfd2d462c65ab6d571472eb731e463 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 24 Apr 2017 21:09:58 -0400 Subject: [PATCH 16/89] update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4d096a61..af8d86526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,9 @@ IMPROVEMENTS: BUG FIXES: + * core: Prevent resource.Retry from adding untracked resources after the timeout: [GH-13778] + * core: Allow a schema.TypeList to be ForceNew and computed [GH-13863] + * core: Fix crash when refresh or apply build an invalid graph [GH-13665] * core: Add the close provider/provisioner transformers back [GH-13102] * core: Fix a crash condition by improving the flatmap.Expand() logic [GH-13541] * provider/alicloud: Fix create PrePaid instance [GH-13662] From 7a07c4e99cceb0d17867352d9e67b8dbc50dfd70 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 24 Apr 2017 21:12:59 -0400 Subject: [PATCH 17/89] update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af8d86526..53f96042b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ FEATURES: IMPROVEMENTS: + + * core: Add a `-reconfigure` flag to the `init` command, to configure a backend while ignoring any saved configuration [GH-13825] * helper/schema: Disallow validation+diff suppression on computed fields [GH-13878] * config: The interpolation function `cidrhost` now accepts a negative host number to count backwards from the end of the range [GH-13765] * config: New interpolation function `matchkeys` for using values from one list to filter corresponding values from another list using a matching set. [GH-13847] From 563cfd00df6a67f95e14367468494d1f8a4c4b26 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 21 Apr 2017 18:57:39 -0400 Subject: [PATCH 18/89] always wrap remote state in a BackupState Use a local backup for remote state operations. This allows for manual recovery in the case of a put failure. --- backend/local/backend.go | 25 +++++++++++-- backend/local/backend_test.go | 66 ++++++++++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/backend/local/backend.go b/backend/local/backend.go index 063766b1e..7c715d67a 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -170,9 +170,30 @@ func (b *Local) DeleteState(name string) error { } func (b *Local) State(name string) (state.State, error) { + statePath, stateOutPath, backupPath := b.StatePaths(name) + // If we have a backend handling state, defer to that. if b.Backend != nil { - return b.Backend.State(name) + s, err := b.Backend.State(name) + if err != nil { + return nil, err + } + + // make sure we always have a backup state, unless it disabled + if backupPath == "" { + return s, nil + } + + // see if the delegated backend returned a BackupState of its own + if s, ok := s.(*state.BackupState); ok { + return s, nil + } + + s = &state.BackupState{ + Real: s, + Path: backupPath, + } + return s, nil } if s, ok := b.states[name]; ok { @@ -183,8 +204,6 @@ func (b *Local) State(name string) (state.State, error) { return nil, err } - statePath, stateOutPath, backupPath := b.StatePaths(name) - // Otherwise, we need to load the state. var s state.State = &state.LocalState{ Path: statePath, diff --git a/backend/local/backend_test.go b/backend/local/backend_test.go index 3b5f1f9bd..a32cbc7d7 100644 --- a/backend/local/backend_test.go +++ b/backend/local/backend_test.go @@ -169,6 +169,11 @@ func TestLocal_addAndRemoveStates(t *testing.T) { // verify it's being called. type testDelegateBackend struct { *Local + + // return a sentinel error on these calls + stateErr bool + statesErr bool + deleteErr bool } var errTestDelegateState = errors.New("State called") @@ -176,22 +181,39 @@ var errTestDelegateStates = errors.New("States called") var errTestDelegateDeleteState = errors.New("Delete called") func (b *testDelegateBackend) State(name string) (state.State, error) { - return nil, errTestDelegateState + if b.stateErr { + return nil, errTestDelegateState + } + s := &state.LocalState{ + Path: "terraform.tfstate", + PathOut: "terraform.tfstate", + } + return s, nil } func (b *testDelegateBackend) States() ([]string, error) { - return nil, errTestDelegateStates + if b.statesErr { + return nil, errTestDelegateStates + } + return []string{"default"}, nil } func (b *testDelegateBackend) DeleteState(name string) error { - return errTestDelegateDeleteState + if b.deleteErr { + return errTestDelegateDeleteState + } + return nil } // verify that the MultiState methods are dispatched to the correct Backend. func TestLocal_multiStateBackend(t *testing.T) { // assign a separate backend where we can read the state b := &Local{ - Backend: &testDelegateBackend{}, + Backend: &testDelegateBackend{ + stateErr: true, + statesErr: true, + deleteErr: true, + }, } if _, err := b.State("test"); err != errTestDelegateState { @@ -205,7 +227,43 @@ func TestLocal_multiStateBackend(t *testing.T) { if err := b.DeleteState("test"); err != errTestDelegateDeleteState { t.Fatal("expected errTestDelegateDeleteState, got:", err) } +} +// verify that a remote state backend is always wrapped in a BackupState +func TestLocal_remoteStateBackup(t *testing.T) { + // assign a separate backend to mock a remote state backend + b := &Local{ + Backend: &testDelegateBackend{}, + } + + s, err := b.State("default") + if err != nil { + t.Fatal(err) + } + + bs, ok := s.(*state.BackupState) + if !ok { + t.Fatal("remote state is not backed up") + } + + if bs.Path != DefaultStateFilename+DefaultBackupExtension { + t.Fatal("bad backup location:", bs.Path) + } + + // do the same with a named state, which should use the local env directories + s, err = b.State("test") + if err != nil { + t.Fatal(err) + } + + bs, ok = s.(*state.BackupState) + if !ok { + t.Fatal("remote state is not backed up") + } + + if bs.Path != filepath.Join(DefaultEnvDir, "test", DefaultStateFilename+DefaultBackupExtension) { + t.Fatal("bad backup location:", bs.Path) + } } // change into a tmp dir and return a deferable func to change back and cleanup From 4622960835d758f3d87637fe2affd214afec46ba Mon Sep 17 00:00:00 2001 From: evalphobia Date: Tue, 25 Apr 2017 12:20:34 +0900 Subject: [PATCH 19/89] Fix import path on provider-localfile --- builtin/bins/provider-localfile/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/bins/provider-localfile/main.go b/builtin/bins/provider-localfile/main.go index 4a98ecfdd..70494016f 100644 --- a/builtin/bins/provider-localfile/main.go +++ b/builtin/bins/provider-localfile/main.go @@ -1,12 +1,12 @@ package main import ( - "github.com/hashicorp/terraform/builtin/providers/localfile" + "github.com/hashicorp/terraform/builtin/providers/local" "github.com/hashicorp/terraform/plugin" ) func main() { plugin.Serve(&plugin.ServeOpts{ - ProviderFunc: localfile.Provider, + ProviderFunc: local.Provider, }) } From 5121995100e3d87561f978c3aed0199bf705da95 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Tue, 25 Apr 2017 10:45:46 +0100 Subject: [PATCH 20/89] Ignoring the case of the create_option field. Fixes #13927. --- .../providers/azurerm/resource_arm_virtual_machine.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/builtin/providers/azurerm/resource_arm_virtual_machine.go b/builtin/providers/azurerm/resource_arm_virtual_machine.go index c3f736412..4a01ca374 100644 --- a/builtin/providers/azurerm/resource_arm_virtual_machine.go +++ b/builtin/providers/azurerm/resource_arm_virtual_machine.go @@ -177,8 +177,9 @@ func resourceArmVirtualMachine() *schema.Resource { }, "create_option": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: ignoreCaseDiffSuppressFunc, }, "disk_size_gb": { @@ -232,8 +233,9 @@ func resourceArmVirtualMachine() *schema.Resource { }, "create_option": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: ignoreCaseDiffSuppressFunc, }, "caching": { From bc46b1cbf948a656583fb1d36c63ee82dc70662b Mon Sep 17 00:00:00 2001 From: Tom Elliff Date: Tue, 25 Apr 2017 14:46:51 +0100 Subject: [PATCH 21/89] Allow filtering of aws_subnet_ids by tags This is the minimal amount of work needed to be able to create a list of a subset of subnet IDs in a VPC, allowing people to loop through them easily when creating EC2 instances or provide a list straight to an ELB. --- .../aws/data_source_aws_subnet_ids.go | 8 +++ .../aws/data_source_aws_subnet_ids_test.go | 68 +++++++++++++++++-- .../providers/aws/d/subnet_ids.html.markdown | 23 +++++++ 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/builtin/providers/aws/data_source_aws_subnet_ids.go b/builtin/providers/aws/data_source_aws_subnet_ids.go index efe6c75a4..c1a495aa1 100644 --- a/builtin/providers/aws/data_source_aws_subnet_ids.go +++ b/builtin/providers/aws/data_source_aws_subnet_ids.go @@ -12,10 +12,14 @@ func dataSourceAwsSubnetIDs() *schema.Resource { return &schema.Resource{ Read: dataSourceAwsSubnetIDsRead, Schema: map[string]*schema.Schema{ + + "tags": tagsSchemaComputed(), + "vpc_id": &schema.Schema{ Type: schema.TypeString, Required: true, }, + "ids": &schema.Schema{ Type: schema.TypeSet, Computed: true, @@ -37,6 +41,10 @@ func dataSourceAwsSubnetIDsRead(d *schema.ResourceData, meta interface{}) error }, ) + req.Filters = append(req.Filters, buildEC2TagFilterList( + tagsFromMap(d.Get("tags").(map[string]interface{})), + )...) + log.Printf("[DEBUG] DescribeSubnets %s\n", req) resp, err := conn.DescribeSubnets(req) if err != nil { diff --git a/builtin/providers/aws/data_source_aws_subnet_ids_test.go b/builtin/providers/aws/data_source_aws_subnet_ids_test.go index 36f6c4b91..5d752a25e 100644 --- a/builtin/providers/aws/data_source_aws_subnet_ids_test.go +++ b/builtin/providers/aws/data_source_aws_subnet_ids_test.go @@ -21,7 +21,8 @@ func TestAccDataSourceAwsSubnetIDs(t *testing.T) { { Config: testAccDataSourceAwsSubnetIDsConfigWithDataSource(rInt), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("data.aws_subnet_ids.selected", "ids.#", "1"), + resource.TestCheckResourceAttr("data.aws_subnet_ids.selected", "ids.#", "3"), + resource.TestCheckResourceAttr("data.aws_subnet_ids.private", "ids.#", "2"), ), }, }, @@ -39,20 +40,50 @@ func testAccDataSourceAwsSubnetIDsConfigWithDataSource(rInt int) string { } } - resource "aws_subnet" "test" { + resource "aws_subnet" "test_public_a" { vpc_id = "${aws_vpc.test.id}" cidr_block = "172.%d.123.0/24" availability_zone = "us-west-2a" tags { - Name = "terraform-testacc-subnet-ids-data-source" + Name = "terraform-testacc-subnet-ids-data-source-public-a" + Tier = "Public" + } + } + + resource "aws_subnet" "test_private_a" { + vpc_id = "${aws_vpc.test.id}" + cidr_block = "172.%d.125.0/24" + availability_zone = "us-west-2a" + + tags { + Name = "terraform-testacc-subnet-ids-data-source-private-a" + Tier = "Private" + } + } + + resource "aws_subnet" "test_private_b" { + vpc_id = "${aws_vpc.test.id}" + cidr_block = "172.%d.126.0/24" + availability_zone = "us-west-2b" + + tags { + Name = "terraform-testacc-subnet-ids-data-source-private-b" + Tier = "Private" } } data "aws_subnet_ids" "selected" { vpc_id = "${aws_vpc.test.id}" } - `, rInt, rInt) + + data "aws_subnet_ids" "private" { + vpc_id = "${aws_vpc.test.id}" + tags { + Tier = "Private" + } + } + `, rInt, rInt, rInt, rInt) } func testAccDataSourceAwsSubnetIDsConfig(rInt int) string { @@ -65,14 +96,37 @@ func testAccDataSourceAwsSubnetIDsConfig(rInt int) string { } } - resource "aws_subnet" "test" { + resource "aws_subnet" "test_public_a" { vpc_id = "${aws_vpc.test.id}" cidr_block = "172.%d.123.0/24" availability_zone = "us-west-2a" tags { - Name = "terraform-testacc-subnet-ids-data-source" + Name = "terraform-testacc-subnet-ids-data-source-public-a" + Tier = "Public" } } - `, rInt, rInt) + + resource "aws_subnet" "test_private_a" { + vpc_id = "${aws_vpc.test.id}" + cidr_block = "172.%d.125.0/24" + availability_zone = "us-west-2a" + + tags { + Name = "terraform-testacc-subnet-ids-data-source-private-a" + Tier = "Private" + } + } + + resource "aws_subnet" "test_private_b" { + vpc_id = "${aws_vpc.test.id}" + cidr_block = "172.%d.126.0/24" + availability_zone = "us-west-2b" + + tags { + Name = "terraform-testacc-subnet-ids-data-source-private-b" + Tier = "Private" + } + } + `, rInt, rInt, rInt, rInt) } diff --git a/website/source/docs/providers/aws/d/subnet_ids.html.markdown b/website/source/docs/providers/aws/d/subnet_ids.html.markdown index 7cf34ffa6..871da0ca2 100644 --- a/website/source/docs/providers/aws/d/subnet_ids.html.markdown +++ b/website/source/docs/providers/aws/d/subnet_ids.html.markdown @@ -31,10 +31,33 @@ output "subnet_cidr_blocks" { } ``` +The following example retrieves a list of all subnets in a VPC with a custom +tag of `Tier` set to a value of "Private" so that the `aws_instance` resource +can loop through the subnets, putting instances across availability zones. + +```hcl +data "aws_subnet_ids" "private" { + vpc_id = "${var.vpc_id}" + tags { + Tier = "Private" + } +} + +resource "aws_instance" "app" { + count = "3" + ami = "${var.ami}" + instance_type = "t2.micro" + subnet_id = "${element(data.aws_subnet_ids.private.ids, count.index)}" +} +``` + ## Argument Reference * `vpc_id` - (Required) The VPC ID that you want to filter from. +* `tags` - (Optional) A mapping of tags, each pair of which must exactly match + a pair on the desired subnets. + ## Attributes Reference * `ids` - Is a list of all the subnet ids found. If none found. This data source will fail out. From af3ba9a02cd85a8dfb3f462a2b9e2fa3972bddae Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Tue, 25 Apr 2017 10:06:28 -0400 Subject: [PATCH 22/89] cleanup conditional logic --- builtin/providers/aws/resource_aws_instance.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/builtin/providers/aws/resource_aws_instance.go b/builtin/providers/aws/resource_aws_instance.go index 481aa4cd1..a29df8993 100644 --- a/builtin/providers/aws/resource_aws_instance.go +++ b/builtin/providers/aws/resource_aws_instance.go @@ -1388,8 +1388,12 @@ func buildAwsInstanceOpts( networkInterfaces, interfacesOk := d.GetOk("network_interface") - // If simply specifying a subnetID, privateIP, Security Groups, or VPC Security Groups, build these now - if !hasSubnet && !associatePublicIPAddress && !interfacesOk { + // If setting subnet and public address, OR manual network interfaces, populate those now. + if hasSubnet && associatePublicIPAddress || interfacesOk { + // Otherwise we're attaching (a) network interface(s) + opts.NetworkInterfaces = buildNetworkInterfaceOpts(d, groups, networkInterfaces) + } else { + // If simply specifying a subnetID, privateIP, Security Groups, or VPC Security Groups, build these now if subnetID != "" { opts.SubnetID = aws.String(subnetID) } @@ -1409,9 +1413,6 @@ func buildAwsInstanceOpts( opts.SecurityGroupIDs = append(opts.SecurityGroupIDs, aws.String(v.(string))) } } - } else { - // Otherwise we're attaching (a) network interface(s) - opts.NetworkInterfaces = buildNetworkInterfaceOpts(d, groups, networkInterfaces) } if v, ok := d.GetOk("key_name"); ok { @@ -1425,7 +1426,6 @@ func buildAwsInstanceOpts( if len(blockDevices) > 0 { opts.BlockDeviceMappings = blockDevices } - return opts, nil } From 414060c83b54a76769233be3e1fa6a295097cbae Mon Sep 17 00:00:00 2001 From: Josh Komoroske Date: Tue, 25 Apr 2017 07:21:49 -0700 Subject: [PATCH 23/89] Documented aws_api_gateway_usage_plan.api_stages attributes as required (#13930) --- .../docs/providers/aws/r/api_gateway_usage_plan.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/docs/providers/aws/r/api_gateway_usage_plan.html.markdown b/website/source/docs/providers/aws/r/api_gateway_usage_plan.html.markdown index ee8b70c1f..e55701523 100644 --- a/website/source/docs/providers/aws/r/api_gateway_usage_plan.html.markdown +++ b/website/source/docs/providers/aws/r/api_gateway_usage_plan.html.markdown @@ -72,8 +72,8 @@ The API Gateway Usage Plan argument layout is a structure composed of several su #### Api Stages arguments - * `api_id` (Optional) - API Id of the associated API stage in a usage plan. - * `stage` (Optional) - API stage name of the associated API stage in a usage plan. + * `api_id` (Required) - API Id of the associated API stage in a usage plan. + * `stage` (Required) - API stage name of the associated API stage in a usage plan. #### Quota Settings Arguments From 8d48c4743c2baf4e10354636a9a007b2dc3a2c04 Mon Sep 17 00:00:00 2001 From: Tom Harvey Date: Tue, 25 Apr 2017 15:22:15 +0100 Subject: [PATCH 24/89] Updating to include #13933 --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53f96042b..407967da8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,7 +85,8 @@ BUG FIXES: * provider/azurerm: Locking around Network Security Group / Subnets [GH-13637] * provider/azurerm: Locking route table on subnet create/delete [GH-13791] * provider/azurerm: VM's - fixes a bug where ssh_keys could contain a null entry [GH-13755] - * provider/azurerm: fixing a bug refreshing the `azurerm_redis_cache` [GH-13899] + * provider/azurerm: VM's - ignoring the case on the `create_option` field during Diff's [GH-13933] + * provider/azurerm: fixing a bug refreshing the `azurerm_redis_cache` [GH-13899] * provider/fastly: Fix issue with using 0 for `default_ttl` [GH-13648] * provider/fastly: Add ability to associate a healthcheck to a backend [GH-13539] * provider/google: Stop setting the id when project creation fails [GH-13644] From 4441c6f53bee67be1c1637cfebee197ef9662c94 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Tue, 25 Apr 2017 10:41:10 -0400 Subject: [PATCH 25/89] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 407967da8..f882347d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ IMPROVEMENTS: * provider/fastly: Add support for GCS logging [GH-13553] * provider/google: `google_compute_address` and `google_compute_global_address` are now importable [GH-13270] * provider/google: `google_compute_network` is now importable [GH-13834] + * provider/heroku: Set App buildpacks from config [GH-13910] * provider/vault: `vault_generic_secret` resource can now optionally detect drift if it has appropriate access [GH-11776] BUG FIXES: From 6ef7c83ec56d2dcc8c8af7a9e0004026abb070e0 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 25 Apr 2017 11:43:59 -0400 Subject: [PATCH 26/89] add data-loss warning to SIGINT handler in apply Warning user about data loss after receiving an interrupt. --- command/apply.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/command/apply.go b/command/apply.go index ca9813eb5..1e49739a0 100644 --- a/command/apply.go +++ b/command/apply.go @@ -201,7 +201,7 @@ func (c *ApplyCommand) Run(args []string) int { ctxCancel() // Notify the user - c.Ui.Output("Interrupt received. Gracefully shutting down...") + c.Ui.Output(outputInterrupt) // Still get the result, since there is still one select { @@ -418,3 +418,7 @@ func outputsAsString(state *terraform.State, modPath []string, schema []*config. return strings.TrimSpace(outputBuf.String()) } + +const outputInterrupt = `Interrupt received. +Please wait for Terraform to exit or data loss may occur. +Gracefully shutting down...` From 7dad3f4d48abebeb71da625b74d9b324bab3f045 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 25 Apr 2017 11:44:51 -0400 Subject: [PATCH 27/89] remove redundant output when interrupting apply The backend apply operation doesn't need to output the same text as the cli itself. Instead notify the user that we are in the process of stopping the operation. --- backend/local/backend_apply.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/local/backend_apply.go b/backend/local/backend_apply.go index 8fec2019e..9bc41f487 100644 --- a/backend/local/backend_apply.go +++ b/backend/local/backend_apply.go @@ -121,7 +121,7 @@ func (b *Local) opApply( select { case <-ctx.Done(): if b.CLI != nil { - b.CLI.Output("Interrupt received. Gracefully shutting down...") + b.CLI.Output("stopping apply operation...") } // Stop execution From 1e0f4d5e5e757fbd8078af032f52d27c6827e5d8 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 25 Apr 2017 11:30:42 -0600 Subject: [PATCH 28/89] Randomizes glaciar vault name --- .../aws/import_aws_glacier_vault_test.go | 4 ++- .../aws/resource_aws_glacier_vault_test.go | 29 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/builtin/providers/aws/import_aws_glacier_vault_test.go b/builtin/providers/aws/import_aws_glacier_vault_test.go index e5fd5aa5b..f7c20666e 100644 --- a/builtin/providers/aws/import_aws_glacier_vault_test.go +++ b/builtin/providers/aws/import_aws_glacier_vault_test.go @@ -3,11 +3,13 @@ package aws import ( "testing" + "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" ) func TestAccAWSGlacierVault_importBasic(t *testing.T) { resourceName := "aws_glacier_vault.full" + rInt := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -15,7 +17,7 @@ func TestAccAWSGlacierVault_importBasic(t *testing.T) { CheckDestroy: testAccCheckGlacierVaultDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccGlacierVault_full, + Config: testAccGlacierVault_full(rInt), }, resource.TestStep{ diff --git a/builtin/providers/aws/resource_aws_glacier_vault_test.go b/builtin/providers/aws/resource_aws_glacier_vault_test.go index 009cfb03d..db2ade620 100644 --- a/builtin/providers/aws/resource_aws_glacier_vault_test.go +++ b/builtin/providers/aws/resource_aws_glacier_vault_test.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/glacier" + "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) @@ -30,13 +31,14 @@ func TestAccAWSGlacierVault_basic(t *testing.T) { } func TestAccAWSGlacierVault_full(t *testing.T) { + rInt := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckGlacierVaultDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccGlacierVault_full, + Config: testAccGlacierVault_full(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckGlacierVaultExists("aws_glacier_vault.full"), ), @@ -46,19 +48,20 @@ func TestAccAWSGlacierVault_full(t *testing.T) { } func TestAccAWSGlacierVault_RemoveNotifications(t *testing.T) { + rInt := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckGlacierVaultDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccGlacierVault_full, + Config: testAccGlacierVault_full(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckGlacierVaultExists("aws_glacier_vault.full"), ), }, resource.TestStep{ - Config: testAccGlacierVault_withoutNotification, + Config: testAccGlacierVault_withoutNotification(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckGlacierVaultExists("aws_glacier_vault.full"), testAccCheckVaultNotificationsMissing("aws_glacier_vault.full"), @@ -211,13 +214,14 @@ resource "aws_glacier_vault" "test" { } ` -const testAccGlacierVault_full = ` +func testAccGlacierVault_full(rInt int) string { + return fmt.Sprintf(` resource "aws_sns_topic" "aws_sns_topic" { - name = "glacier-sns-topic" + name = "glacier-sns-topic-%d" } resource "aws_glacier_vault" "full" { - name = "my_test_vault" + name = "my_test_vault_%d" notification { sns_topic = "${aws_sns_topic.aws_sns_topic.arn}" events = ["ArchiveRetrievalCompleted","InventoryRetrievalCompleted"] @@ -226,17 +230,20 @@ resource "aws_glacier_vault" "full" { Test="Test1" } } -` +`, rInt, rInt) +} -const testAccGlacierVault_withoutNotification = ` +func testAccGlacierVault_withoutNotification(rInt int) string { + return fmt.Sprintf(` resource "aws_sns_topic" "aws_sns_topic" { - name = "glacier-sns-topic" + name = "glacier-sns-topic-%d" } resource "aws_glacier_vault" "full" { - name = "my_test_vault" + name = "my_test_vault_%d" tags { Test="Test1" } } -` +`, rInt, rInt) +} From 7130755d8bc9a105b29626ddf7e8db11b7e16d5d Mon Sep 17 00:00:00 2001 From: = Date: Tue, 25 Apr 2017 11:44:42 -0600 Subject: [PATCH 29/89] Randomizes basic test name --- .../providers/aws/resource_aws_glacier_vault_test.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/builtin/providers/aws/resource_aws_glacier_vault_test.go b/builtin/providers/aws/resource_aws_glacier_vault_test.go index db2ade620..011284c1d 100644 --- a/builtin/providers/aws/resource_aws_glacier_vault_test.go +++ b/builtin/providers/aws/resource_aws_glacier_vault_test.go @@ -15,13 +15,14 @@ import ( ) func TestAccAWSGlacierVault_basic(t *testing.T) { + rInt := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckGlacierVaultDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccGlacierVault_basic, + Config: testAccGlacierVault_basic(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckGlacierVaultExists("aws_glacier_vault.test"), ), @@ -208,11 +209,13 @@ func testAccCheckGlacierVaultDestroy(s *terraform.State) error { return nil } -const testAccGlacierVault_basic = ` +func testAccGlacierVault_basic(rInt int) string { + return fmt.Sprintf(` resource "aws_glacier_vault" "test" { - name = "my_test_vault" + name = "my_test_vault_%d" +} +`, rInt) } -` func testAccGlacierVault_full(rInt int) string { return fmt.Sprintf(` From f721608e4ebcac09453ee646adf7545efc7abfec Mon Sep 17 00:00:00 2001 From: Quentin Machu Date: Thu, 13 Apr 2017 16:48:51 -0700 Subject: [PATCH 30/89] provider/template: Add a 'dir' resource to template entire directories When TerraForm is used to configure and deploy infrastructure applications that require dozens templated files, such as Kubernetes, it becomes extremely burdensome to template them individually: each of them requires a data source block as well as an upload/export (file provisioner, AWS S3, ...). Instead, this commit introduces a mean to template an entire folder of files (recursively), that can then be treated as a whole by any provider or provisioner that support directory inputs (such as the file provisioner, the archive provider, ...). This does not intend to make TerraForm a full-fledged templating system as the templating grammar and capabilities are left unchanged. This only aims at improving the user-experience of the existing templating provider by significantly reducing the overhead when several files are to be generated - without forcing the users to rely on external tools when these templates stay simple and that their generation in TerraForm is justified. --- builtin/providers/template/provider.go | 1 + .../template/resource_template_dir.go | 225 ++++++++++++++++++ .../template/resource_template_dir_test.go | 104 ++++++++ .../docs/providers/template/r/dir.html.md | 58 +++++ website/source/layouts/template.erb | 9 + 5 files changed, 397 insertions(+) create mode 100644 builtin/providers/template/resource_template_dir.go create mode 100644 builtin/providers/template/resource_template_dir_test.go create mode 100644 website/source/docs/providers/template/r/dir.html.md diff --git a/builtin/providers/template/provider.go b/builtin/providers/template/provider.go index ece6c9f34..fb340754d 100644 --- a/builtin/providers/template/provider.go +++ b/builtin/providers/template/provider.go @@ -20,6 +20,7 @@ func Provider() terraform.ResourceProvider { "template_cloudinit_config", dataSourceCloudinitConfig(), ), + "template_dir": resourceDir(), }, } } diff --git a/builtin/providers/template/resource_template_dir.go b/builtin/providers/template/resource_template_dir.go new file mode 100644 index 000000000..583926bb0 --- /dev/null +++ b/builtin/providers/template/resource_template_dir.go @@ -0,0 +1,225 @@ +package template + +import ( + "archive/tar" + "bytes" + "crypto/sha1" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + + "github.com/hashicorp/terraform/helper/pathorcontents" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceDir() *schema.Resource { + return &schema.Resource{ + Create: resourceTemplateDirCreate, + Read: resourceTemplateDirRead, + Delete: resourceTemplateDirDelete, + + Schema: map[string]*schema.Schema{ + "source_dir": { + Type: schema.TypeString, + Description: "Path to the directory where the files to template reside", + Required: true, + ForceNew: true, + }, + "vars": { + Type: schema.TypeMap, + Optional: true, + Default: make(map[string]interface{}), + Description: "Variables to substitute", + ValidateFunc: validateVarsAttribute, + ForceNew: true, + }, + "destination_dir": { + Type: schema.TypeString, + Description: "Path to the directory where the templated files will be written", + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceTemplateDirRead(d *schema.ResourceData, meta interface{}) error { + sourceDir := d.Get("source_dir").(string) + destinationDir := d.Get("destination_dir").(string) + + // If the output doesn't exist, mark the resource for creation. + if _, err := os.Stat(destinationDir); os.IsNotExist(err) { + d.SetId("") + return nil + } + + // If the combined hash of the input and output directories is different from + // the stored one, mark the resource for re-creation. + // + // The output directory is technically enough for the general case, but by + // hashing the input directory as well, we make development much easier: when + // a developer modifies one of the input files, the generation is + // re-triggered. + hash, err := generateID(sourceDir, destinationDir) + if err != nil { + return err + } + if hash != d.Id() { + d.SetId("") + return nil + } + + return nil +} + +func resourceTemplateDirCreate(d *schema.ResourceData, meta interface{}) error { + sourceDir := d.Get("source_dir").(string) + destinationDir := d.Get("destination_dir").(string) + vars := d.Get("vars").(map[string]interface{}) + + // Always delete the output first, otherwise files that got deleted from the + // input directory might still be present in the output afterwards. + if err := resourceTemplateDirDelete(d, meta); err != nil { + return err + } + + // Recursively crawl the input files/directories and generate the output ones. + err := filepath.Walk(sourceDir, func(p string, f os.FileInfo, err error) error { + if f.IsDir() { + return nil + } + if err != nil { + return err + } + + relPath, _ := filepath.Rel(sourceDir, p) + return generateDirFile(p, path.Join(destinationDir, relPath), f, vars) + }) + if err != nil { + return err + } + + // Compute ID. + hash, err := generateID(sourceDir, destinationDir) + if err != nil { + return err + } + d.SetId(hash) + + return nil +} + +func resourceTemplateDirDelete(d *schema.ResourceData, _ interface{}) error { + d.SetId("") + + destinationDir := d.Get("destination_dir").(string) + if _, err := os.Stat(destinationDir); os.IsNotExist(err) { + return nil + } + + if err := os.RemoveAll(destinationDir); err != nil { + return fmt.Errorf("could not delete directory %q: %s", destinationDir, err) + } + + return nil +} + +func generateDirFile(sourceDir, destinationDir string, f os.FileInfo, vars map[string]interface{}) error { + inputContent, _, err := pathorcontents.Read(sourceDir) + if err != nil { + return err + } + + outputContent, err := execute(inputContent, vars) + if err != nil { + return templateRenderError(fmt.Errorf("failed to render %v: %v", sourceDir, err)) + } + + outputDir := path.Dir(destinationDir) + if _, err := os.Stat(outputDir); err != nil { + if err := os.MkdirAll(outputDir, 0777); err != nil { + return err + } + } + + err = ioutil.WriteFile(destinationDir, []byte(outputContent), f.Mode()) + if err != nil { + return err + } + + return nil +} + +func generateID(sourceDir, destinationDir string) (string, error) { + inputHash, err := generateDirHash(sourceDir) + if err != nil { + return "", err + } + outputHash, err := generateDirHash(destinationDir) + if err != nil { + return "", err + } + checksum := sha1.Sum([]byte(inputHash + outputHash)) + return hex.EncodeToString(checksum[:]), nil +} + +func generateDirHash(directoryPath string) (string, error) { + tarData, err := tarDir(directoryPath) + if err != nil { + return "", fmt.Errorf("could not generate output checksum: %s", err) + } + + checksum := sha1.Sum(tarData) + return hex.EncodeToString(checksum[:]), nil +} + +func tarDir(directoryPath string) ([]byte, error) { + buf := new(bytes.Buffer) + tw := tar.NewWriter(buf) + + writeFile := func(p string, f os.FileInfo, err error) error { + if err != nil { + return err + } + + var header *tar.Header + var file *os.File + + header, err = tar.FileInfoHeader(f, f.Name()) + if err != nil { + return err + } + relPath, _ := filepath.Rel(directoryPath, p) + header.Name = relPath + + if err := tw.WriteHeader(header); err != nil { + return err + } + + if f.IsDir() { + return nil + } + + file, err = os.Open(p) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(tw, file) + return err + } + + if err := filepath.Walk(directoryPath, writeFile); err != nil { + return []byte{}, err + } + if err := tw.Flush(); err != nil { + return []byte{}, err + } + + return buf.Bytes(), nil +} diff --git a/builtin/providers/template/resource_template_dir_test.go b/builtin/providers/template/resource_template_dir_test.go new file mode 100644 index 000000000..716a5f0af --- /dev/null +++ b/builtin/providers/template/resource_template_dir_test.go @@ -0,0 +1,104 @@ +package template + +import ( + "fmt" + "testing" + + "errors" + r "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "io/ioutil" + "os" + "path/filepath" +) + +const templateDirRenderingConfig = ` +resource "template_dir" "dir" { + source_dir = "%s" + destination_dir = "%s" + vars = %s +}` + +type testTemplate struct { + template string + want string +} + +func testTemplateDirWriteFiles(files map[string]testTemplate) (in, out string, err error) { + in, err = ioutil.TempDir(os.TempDir(), "terraform_template_dir") + if err != nil { + return + } + + for name, file := range files { + path := filepath.Join(in, name) + + err = os.MkdirAll(filepath.Dir(path), 0777) + if err != nil { + return + } + + err = ioutil.WriteFile(path, []byte(file.template), 0777) + if err != nil { + return + } + } + + out = fmt.Sprintf("%s.out", in) + return +} + +func TestTemplateDirRendering(t *testing.T) { + var cases = []struct { + vars string + files map[string]testTemplate + }{ + { + files: map[string]testTemplate{ + "foo.txt": {"${bar}", "bar"}, + "nested/monkey.txt": {"ooh-ooh-ooh-eee-eee", "ooh-ooh-ooh-eee-eee"}, + "maths.txt": {"${1+2+3}", "6"}, + }, + vars: `{bar = "bar"}`, + }, + } + + for _, tt := range cases { + // Write the desired templates in a temporary directory. + in, out, err := testTemplateDirWriteFiles(tt.files) + if err != nil { + t.Skipf("could not write templates to temporary directory: %s", err) + continue + } + defer os.RemoveAll(in) + defer os.RemoveAll(out) + + // Run test case. + r.UnitTest(t, r.TestCase{ + Providers: testProviders, + Steps: []r.TestStep{ + { + Config: fmt.Sprintf(templateDirRenderingConfig, in, out, tt.vars), + Check: func(s *terraform.State) error { + for name, file := range tt.files { + content, err := ioutil.ReadFile(filepath.Join(out, name)) + if err != nil { + return fmt.Errorf("template:\n%s\nvars:\n%s\ngot:\n%s\nwant:\n%s\n", file.template, tt.vars, err, file.want) + } + if string(content) != file.want { + return fmt.Errorf("template:\n%s\nvars:\n%s\ngot:\n%s\nwant:\n%s\n", file.template, tt.vars, content, file.want) + } + } + return nil + }, + }, + }, + CheckDestroy: func(*terraform.State) error { + if _, err := os.Stat(out); os.IsNotExist(err) { + return nil + } + return errors.New("template_dir did not get destroyed") + }, + }) + } +} diff --git a/website/source/docs/providers/template/r/dir.html.md b/website/source/docs/providers/template/r/dir.html.md new file mode 100644 index 000000000..7e0c03067 --- /dev/null +++ b/website/source/docs/providers/template/r/dir.html.md @@ -0,0 +1,58 @@ +--- +layout: "template" +page_title: "Template: template_dir" +sidebar_current: "docs-template-resource-dir" +description: |- + Renders templates from a directory. +--- + +# template_dir + +Renders templates from a directory. + +## Example Usage +```hcl +data "template_directory" "init" { + source_dir = "${path.cwd}/templates" + destination_dir = "${path.cwd}/templates.generated" + + vars { + consul_address = "${aws_instance.consul.private_ip}" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `source_path` - (Required) Path to the directory where the files to template reside. + +* `destination_path` - (Required) Path to the directory where the templated files will be written. + +* `vars` - (Optional) Variables for interpolation within the template. Note + that variables must all be primitives. Direct references to lists or maps + will cause a validation error. + +NOTE: Any required parent directories are created automatically. Additionally, any external modification to either the files in the source or destination directories will trigger the resource to be re-created. + +## Template Syntax + +The syntax of the template files is the same as +[standard interpolation syntax](/docs/configuration/interpolation.html), +but you only have access to the variables defined in the `vars` section. + +To access interpolations that are normally available to Terraform +configuration (such as other variables, resource attributes, module +outputs, etc.) you'll have to expose them via `vars` as shown below: + +```hcl +resource "template_dir" "init" { + # ... + + vars { + foo = "${var.foo}" + attr = "${aws_instance.foo.private_ip}" + } +} +``` \ No newline at end of file diff --git a/website/source/layouts/template.erb b/website/source/layouts/template.erb index 8416a3dc8..045e95811 100644 --- a/website/source/layouts/template.erb +++ b/website/source/layouts/template.erb @@ -21,6 +21,15 @@ + + > + Resources + + <% end %> From eda2550074e0b7c8aa1a2b5cd1ba94daf5584903 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 25 Apr 2017 10:03:09 -0700 Subject: [PATCH 31/89] provider/template: template_dir: don't crash if source dir nonexistent When an error is passed, the FileInfo can be nil, which was previously causing a crash on trying to evaluate f.IsDir(). By checking for an error first we avoid this crash. --- builtin/providers/template/resource_template_dir.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/providers/template/resource_template_dir.go b/builtin/providers/template/resource_template_dir.go index 583926bb0..9154d2a89 100644 --- a/builtin/providers/template/resource_template_dir.go +++ b/builtin/providers/template/resource_template_dir.go @@ -89,12 +89,12 @@ func resourceTemplateDirCreate(d *schema.ResourceData, meta interface{}) error { // Recursively crawl the input files/directories and generate the output ones. err := filepath.Walk(sourceDir, func(p string, f os.FileInfo, err error) error { - if f.IsDir() { - return nil - } if err != nil { return err } + if f.IsDir() { + return nil + } relPath, _ := filepath.Rel(sourceDir, p) return generateDirFile(p, path.Join(destinationDir, relPath), f, vars) From eaac9fbca3aebd1d1f23ff2b70089739872b166a Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 25 Apr 2017 10:20:21 -0700 Subject: [PATCH 32/89] provider/template: template_dir explicitly create dest dir Previously we were letting it get implicitly created as part of making the structure for copying in each file, but that isn't sufficient if the source directory is empty. By explicitly creating the directory first we ensure that it will complete successfully even in the case of an empty directory. --- builtin/providers/template/resource_template_dir.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/builtin/providers/template/resource_template_dir.go b/builtin/providers/template/resource_template_dir.go index 9154d2a89..63a2f18dc 100644 --- a/builtin/providers/template/resource_template_dir.go +++ b/builtin/providers/template/resource_template_dir.go @@ -87,11 +87,20 @@ func resourceTemplateDirCreate(d *schema.ResourceData, meta interface{}) error { return err } + // Create the destination directory and any other intermediate directories + // leading to it. + if _, err := os.Stat(destinationDir); err != nil { + if err := os.MkdirAll(destinationDir, 0777); err != nil { + return err + } + } + // Recursively crawl the input files/directories and generate the output ones. err := filepath.Walk(sourceDir, func(p string, f os.FileInfo, err error) error { if err != nil { return err } + if f.IsDir() { return nil } From da49171b0645ce3dcd771fdf2eb6337710447718 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 25 Apr 2017 10:46:55 -0700 Subject: [PATCH 33/89] website: flesh out docs for template_dir resource --- .../docs/providers/template/r/dir.html.md | 80 ++++++++++++++++--- 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/website/source/docs/providers/template/r/dir.html.md b/website/source/docs/providers/template/r/dir.html.md index 7e0c03067..9ce700826 100644 --- a/website/source/docs/providers/template/r/dir.html.md +++ b/website/source/docs/providers/template/r/dir.html.md @@ -3,38 +3,87 @@ layout: "template" page_title: "Template: template_dir" sidebar_current: "docs-template-resource-dir" description: |- - Renders templates from a directory. + Renders a directory of templates. --- # template_dir -Renders templates from a directory. +Renders a directory containing templates into a separate directory of +corresponding rendered files. + +`template_dir` is similar to [`template_file`](../d/file.html) but it walks +a given source directory and treats every file it encounters as a template, +rendering it to a corresponding file in the destination directory. + +~> **Note** When working with local files, Terraform will detect the resource +as having been deleted each time a configuration is applied on a new machine +where the destination dir is not present and will generate a diff to create +it. This may cause "noise" in diffs in environments where configurations are +routinely applied by many different users or within automation systems. ## Example Usage + +The following example shows how one might use this resource to produce a +directory of configuration files to upload to a compute instance, using +Amazon EC2 as a placeholder. + ```hcl -data "template_directory" "init" { - source_dir = "${path.cwd}/templates" - destination_dir = "${path.cwd}/templates.generated" +resource "template_dir" "config" { + source_dir = "${path.module}/instance_config_templates" + destination_dir = "${path.cwd}/instance_config" vars { - consul_address = "${aws_instance.consul.private_ip}" + consul_addr = "${var.consul_addr}" } } + +resource "aws_instance" "server" { + ami = "${var.server_ami}" + instance_type = "t2.micro" + + connection { + # ...connection configuration... + } + + provisioner "file" { + # Referencing the template_dir resource ensures that it will be + # created or updated before this aws_instance resource is provisioned. + source = "${template_dir.config.destination_dir}" + destination = "/etc/myapp" + } +} + +variable "consul_addr" {} + +variable "server_ami" {} ``` ## Argument Reference The following arguments are supported: -* `source_path` - (Required) Path to the directory where the files to template reside. +* `source_dir` - (Required) Path to the directory where the files to template reside. -* `destination_path` - (Required) Path to the directory where the templated files will be written. +* `destination_dir` - (Required) Path to the directory where the templated files will be written. * `vars` - (Optional) Variables for interpolation within the template. Note that variables must all be primitives. Direct references to lists or maps will cause a validation error. -NOTE: Any required parent directories are created automatically. Additionally, any external modification to either the files in the source or destination directories will trigger the resource to be re-created. +Any required parent directories of `destination_dir` will be created +automatically, and any pre-existing file or directory at that location will +be deleted before template rendering begins. + +After rendering this resource remembers the content of both the source and +destination directories in the Terraform state, and will plan to recreate the +output directory if any changes are detected during the plan phase. + +Note that it is _not_ safe to use the `file` interpolation function to read +files create by this resource, since that function can be evaluated before the +destination directory has been created or updated. It *is* safe to use the +generated files with resources that directly take filenames as arguments, +as long as the path is constructed using the `destination_dir` attribute +to create a dependency relationship with the `template_dir` resource. ## Template Syntax @@ -44,7 +93,7 @@ but you only have access to the variables defined in the `vars` section. To access interpolations that are normally available to Terraform configuration (such as other variables, resource attributes, module -outputs, etc.) you'll have to expose them via `vars` as shown below: +outputs, etc.) you can expose them via `vars` as shown below: ```hcl resource "template_dir" "init" { @@ -55,4 +104,13 @@ resource "template_dir" "init" { attr = "${aws_instance.foo.private_ip}" } } -``` \ No newline at end of file +``` + +## Attributes + +This resource exports the following attributes: + +* `destination_dir` - The destination directory given in configuration. + Interpolate this attribute into other resource configurations to create + a dependency to ensure that the destination directory is populated before + another resource attempts to read it. From 423bc0dfaac501311ca36ba5fd1d354349bb3e4f Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 25 Apr 2017 11:17:25 -0700 Subject: [PATCH 34/89] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f882347d8..903bcf1bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ FEATURES: * **New Resource:**  `aws_network_interface_attachment` [GH-13861] * **New Resource:** `github_branch_protection` [GH-10476] * **New Resource:** `google_bigquery_dataset` [GH-13436] +* **New Resource:** `template_dir` for producing a directory from templates [GH-13652] * **New Interpolation Function:** `coalescelist()` [GH-12537] From 77b996df6a0ec49d95e0172495de8b0b2e700581 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Tue, 25 Apr 2017 15:23:35 -0400 Subject: [PATCH 35/89] provider/opc: Update documentation Updates documentation to add code-block tags, and populate the importability documentation page. --- .../source/docs/import/importability.html.md | 24 +++++++++++++++++++ ...pc_compute_network_interface.html.markdown | 2 +- .../opc/d/opc_compute_vnic.html.markdown | 2 +- .../docs/providers/opc/index.html.markdown | 2 +- .../opc/r/opc_compute_acl.html.markdown | 6 ++--- .../r/opc_compute_image_list.html.markdown | 6 ++--- ...opc_compute_image_list_entry.html.markdown | 6 ++--- .../opc/r/opc_compute_instance.html.markdown | 6 ++--- ...mpute_ip_address_association.html.markdown | 6 ++--- ...ompute_ip_address_prefix_set.html.markdown | 6 ++--- ...mpute_ip_address_reservation.html.markdown | 10 +++++++- .../opc_compute_ip_association.html.markdown | 6 ++--- .../r/opc_compute_ip_network.html.markdown | 10 +++++++- ..._compute_ip_network_exchange.html.markdown | 6 ++--- .../opc_compute_ip_reservation.html.markdown | 6 ++--- .../opc/r/opc_compute_route.html.markdown | 6 ++--- .../opc/r/opc_compute_sec_rule.html.markdown | 6 ++--- ...compute_security_application.html.markdown | 8 +++---- ...compute_security_association.html.markdown | 6 ++--- ...opc_compute_security_ip_list.html.markdown | 6 ++--- .../r/opc_compute_security_list.html.markdown | 6 ++--- ...pc_compute_security_protocol.html.markdown | 6 ++--- .../r/opc_compute_security_rule.html.markdown | 6 ++--- .../opc/r/opc_compute_ssh_key.html.markdown | 6 ++--- .../opc_compute_storage_volume.html.markdown | 8 +++---- ...pute_storage_volume_snapshot.html.markdown | 6 ++--- .../opc/r/opc_compute_vnic_set.html.markdown | 6 ++--- 27 files changed, 110 insertions(+), 70 deletions(-) diff --git a/website/source/docs/import/importability.html.md b/website/source/docs/import/importability.html.md index 8fbdb4878..b6b22e609 100644 --- a/website/source/docs/import/importability.html.md +++ b/website/source/docs/import/importability.html.md @@ -174,6 +174,30 @@ To make a resource importable, please see the * openstack_networking_secgroup_v2 * openstack_networking_subnet_v2 +### OPC (Oracle Public Cloud) + +* opc_compute_acl +* opc_compute_image_list +* opc_compute_instance +* opc_compute_ip_address_association +* opc_compute_ip_address_prefix_set +* opc_compute_ip_address_reservation +* opc_compute_ip_association +* opc_compute_ip_network_exchange +* opc_compute_ip_network +* opc_compute_ip_reservation +* opc_compute_route +* opc_compute_sec_rule +* opc_compute_security_application +* opc_compute_security_association +* opc_compute_security_ip_list +* opc_compute_security_list +* opc_compute_security_protocol +* opc_compute_security_rule +* opc_compute_ssh_key +* opc_compute_storage_volume_snapshot +* opc_compute_storage_volume + ### PostgreSQL * postgresql_database diff --git a/website/source/docs/providers/opc/d/opc_compute_network_interface.html.markdown b/website/source/docs/providers/opc/d/opc_compute_network_interface.html.markdown index ab924e5d4..8ab2d31cb 100644 --- a/website/source/docs/providers/opc/d/opc_compute_network_interface.html.markdown +++ b/website/source/docs/providers/opc/d/opc_compute_network_interface.html.markdown @@ -12,7 +12,7 @@ Use this data source to access the configuration of an instance's network interf ## Example Usage -``` +```hcl data "opc_compute_network_interface" "foo" { instance_id = "${opc_compute_instance.my_instance.id}" instance_name = "${opc_compute_instance.my_instance.name}" diff --git a/website/source/docs/providers/opc/d/opc_compute_vnic.html.markdown b/website/source/docs/providers/opc/d/opc_compute_vnic.html.markdown index 8656c85c2..bef1b5c66 100644 --- a/website/source/docs/providers/opc/d/opc_compute_vnic.html.markdown +++ b/website/source/docs/providers/opc/d/opc_compute_vnic.html.markdown @@ -12,7 +12,7 @@ Use this data source to access the configuration of a Virtual NIC. ## Example Usage -``` +```hcl data "opc_compute_vnic" "current" { name = "my_vnic_name" } diff --git a/website/source/docs/providers/opc/index.html.markdown b/website/source/docs/providers/opc/index.html.markdown index b23d54687..13c626ead 100644 --- a/website/source/docs/providers/opc/index.html.markdown +++ b/website/source/docs/providers/opc/index.html.markdown @@ -14,7 +14,7 @@ Use the navigation to the left to read about the available resources. ## Example Usage -``` +```hcl # Configure the Oracle Public Cloud provider "opc" { user = "..." diff --git a/website/source/docs/providers/opc/r/opc_compute_acl.html.markdown b/website/source/docs/providers/opc/r/opc_compute_acl.html.markdown index b69aab166..ffc37a29d 100644 --- a/website/source/docs/providers/opc/r/opc_compute_acl.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_acl.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_acl`` resource creates and manages an ACL in an OPC identity d ## Example Usage -``` +```hcl resource "opc_compute_acl" "default" { name = "ACL1" description = "This is a description for an acl" @@ -40,6 +40,6 @@ In addition to the above, the following values are exported: ACL's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_acl.acl1 example +```shell +$ terraform import opc_compute_acl.acl1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_image_list.html.markdown b/website/source/docs/providers/opc/r/opc_compute_image_list.html.markdown index 727114c2e..1e10ec352 100644 --- a/website/source/docs/providers/opc/r/opc_compute_image_list.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_image_list.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_image_list`` resource creates and manages an Image List in an ## Example Usage -``` +```hcl resource "opc_compute_image_list" "test" { name = "imagelist1" description = "This is a description of the Image List" @@ -34,6 +34,6 @@ The following arguments are supported: Image List's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_image_list.imagelist1 example +```shell +$ terraform import opc_compute_image_list.imagelist1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_image_list_entry.html.markdown b/website/source/docs/providers/opc/r/opc_compute_image_list_entry.html.markdown index b062773e8..293bb6c4b 100644 --- a/website/source/docs/providers/opc/r/opc_compute_image_list_entry.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_image_list_entry.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_image_list_entry`` resource creates and manages an Image List ## Example Usage -``` +```hcl resource "opc_compute_image_list" "test" { name = "imagelist1" description = "This is a description of the Image List" @@ -53,6 +53,6 @@ In addition to the above arguments, the following attributes are exported Image List's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_image_list_entry.entry1 example +```shell +$ terraform import opc_compute_image_list_entry.entry1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_instance.html.markdown b/website/source/docs/providers/opc/r/opc_compute_instance.html.markdown index 881045b42..40dcd7167 100644 --- a/website/source/docs/providers/opc/r/opc_compute_instance.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_instance.html.markdown @@ -18,7 +18,7 @@ on your instance resources as an extra safety measure. ## Example Usage -``` +```hcl resource "opc_compute_ip_network" "test" { name = "internal-network" description = "Terraform Provisioned Internal Network" @@ -193,6 +193,6 @@ For example, in the Web Console an instance's fully qualified name is: The instance can be imported as such: -``` -terraform import opc_compute_instance.instance1 instance_name/instance_id +```shell +$ terraform import opc_compute_instance.instance1 instance_name/instance_id ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_ip_address_association.html.markdown b/website/source/docs/providers/opc/r/opc_compute_ip_address_association.html.markdown index 6b63ce55a..cffabad6a 100644 --- a/website/source/docs/providers/opc/r/opc_compute_ip_address_association.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_ip_address_association.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_ip_address_association`` resource creates and manages an IP ad ## Example Usage -``` +```hcl resource "opc_compute_ip_address_association" "default" { name = "PrefixSet1" ip_address_reservation = "${opc_compute_ip_address_reservation.default.name}" @@ -43,6 +43,6 @@ In addition to the above, the following variables are exported: IP Address Associations can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_ip_address_association.default example +```shell +$ terraform import opc_compute_ip_address_association.default example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_ip_address_prefix_set.html.markdown b/website/source/docs/providers/opc/r/opc_compute_ip_address_prefix_set.html.markdown index b4ff5c949..fa63f0794 100644 --- a/website/source/docs/providers/opc/r/opc_compute_ip_address_prefix_set.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_ip_address_prefix_set.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_ip_address_prefix_set`` resource creates and manages an IP add ## Example Usage -``` +```hcl resource "opc_compute_ip_address_prefix_set" "default" { name = "PrefixSet1" prefixes = ["192.168.0.0/16", "172.120.0.0/24"] @@ -40,6 +40,6 @@ In addition to the above, the following variables are exported: IP Address Prefix Set can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_ip_address_prefix_set.default example +```shell +$ terraform import opc_compute_ip_address_prefix_set.default example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_ip_address_reservation.html.markdown b/website/source/docs/providers/opc/r/opc_compute_ip_address_reservation.html.markdown index 6a92b8cae..30aed5c62 100644 --- a/website/source/docs/providers/opc/r/opc_compute_ip_address_reservation.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_ip_address_reservation.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_ip_address_reservation`` resource creates and manages an IP ad ## Example Usage -``` +```hcl resource "opc_compute_ip_address_reservation" "default" { name = "IPAddressReservation1" ip_address_pool = "public-ippool" @@ -36,3 +36,11 @@ In addition to the above, the following attributes are exported: * `ip_address` - Reserved NAT IPv4 address from the IP address pool. * `uri` - The Uniform Resource Identifier of the ip address reservation + +## Import + +IP Address Reservations can be imported using the `resource name`, e.g. + +```shell +$ terraform import opc_compute_ip_address_reservation.default example +``` diff --git a/website/source/docs/providers/opc/r/opc_compute_ip_association.html.markdown b/website/source/docs/providers/opc/r/opc_compute_ip_association.html.markdown index 8a7c073a2..8ed8334c6 100644 --- a/website/source/docs/providers/opc/r/opc_compute_ip_association.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_ip_association.html.markdown @@ -13,7 +13,7 @@ an OPC identity domain, for the Shared Network. ## Example Usage -``` +```hcl resource "opc_compute_ip_association" "instance1_reservation1" { vcable = "${opc_compute_instance.test_instance.vcable}" parentpool = "ipreservation:${opc_compute_ip_reservation.reservation1.name}" @@ -41,6 +41,6 @@ The following attributes are exported: IP Associations can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_ip_association.association1 example +```shell +$ terraform import opc_compute_ip_association.association1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_ip_network.html.markdown b/website/source/docs/providers/opc/r/opc_compute_ip_network.html.markdown index 6a8885337..5b843219d 100644 --- a/website/source/docs/providers/opc/r/opc_compute_ip_network.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_ip_network.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_ip_network`` resource creates and manages an IP Network. ## Example Usage -``` +```hcl resource "opc_compute_ip_network" "foo" { name = "my-ip-network" description = "my IP Network" @@ -52,3 +52,11 @@ The following attributes are exported: * `public_napt_enabled` - Whether public internet access using NAPT for VNICs without any public IP Reservation or not. * `uri` - Uniform Resource Identifier for the IP Network + +## Import + +IP Networks can be imported using the `resource name`, e.g. + +```shell +$ terraform import opc_compute_ip_network.default example +``` diff --git a/website/source/docs/providers/opc/r/opc_compute_ip_network_exchange.html.markdown b/website/source/docs/providers/opc/r/opc_compute_ip_network_exchange.html.markdown index 0f07781f6..e0ab547b1 100644 --- a/website/source/docs/providers/opc/r/opc_compute_ip_network_exchange.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_ip_network_exchange.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_ip_network_exchange`` resource creates and manages an IP netwo ## Example Usage -``` +```hcl resource "opc_compute_ip_network_exchange" "default" { name = "NetworkExchange1" } @@ -32,6 +32,6 @@ The following arguments are supported: IP Network Exchange's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_ip_network_exchange.exchange1 example +```shell +$ terraform import opc_compute_ip_network_exchange.exchange1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_ip_reservation.html.markdown b/website/source/docs/providers/opc/r/opc_compute_ip_reservation.html.markdown index 67632c92d..b1462261b 100644 --- a/website/source/docs/providers/opc/r/opc_compute_ip_reservation.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_ip_reservation.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_ip_reservation`` resource creates and manages an IP reservatio ## Example Usage -``` +```hcl resource "opc_compute_ip_reservation" "reservation1" { parent_pool = "/oracle/public/ippool" permanent = true @@ -38,6 +38,6 @@ deleted and recreated (if false). IP Reservations can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_ip_reservations.reservation1 example +```shell +$ terraform import opc_compute_ip_reservations.reservation1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_route.html.markdown b/website/source/docs/providers/opc/r/opc_compute_route.html.markdown index f7bfe7e63..ccb3d0dac 100644 --- a/website/source/docs/providers/opc/r/opc_compute_route.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_route.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_route`` resource creates and manages a route for an IP Network ## Example Usage -``` +```hcl resource "opc_compute_route" "foo" { name = "my-route" description = "my IP Network route" @@ -55,6 +55,6 @@ The following attributes are exported: Route's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_route.route1 example +```shell +$ terraform import opc_compute_route.route1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_sec_rule.html.markdown b/website/source/docs/providers/opc/r/opc_compute_sec_rule.html.markdown index 2593ad007..96a46b919 100644 --- a/website/source/docs/providers/opc/r/opc_compute_sec_rule.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_sec_rule.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_sec_rule`` resource creates and manages a sec rule in an OPC i ## Example Usage -``` +```hcl resource "opc_compute_sec_rule" "test_rule" { name = "test" source_list = "seclist:${opc_compute_security_list.sec-list1.name}" @@ -52,6 +52,6 @@ In addition to the above, the following values are exported: Sec Rule's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_sec_rule.rule1 example +```shell +$ terraform import opc_compute_sec_rule.rule1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_security_application.html.markdown b/website/source/docs/providers/opc/r/opc_compute_security_application.html.markdown index 0949f9467..2bf7fec5e 100644 --- a/website/source/docs/providers/opc/r/opc_compute_security_application.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_security_application.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_security_application`` resource creates and manages a security ## Example Usage (TCP) -``` +```hcl resource "opc_compute_security_application" "tomcat" { name = "tomcat" protocol = "tcp" @@ -22,7 +22,7 @@ resource "opc_compute_security_application" "tomcat" { ## Example Usage (ICMP) -``` +```hcl resource "opc_compute_security_application" "tomcat" { name = "tomcat" protocol = "icmp" @@ -52,6 +52,6 @@ The following arguments are supported: Security Application's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_security_application.application1 example +```shell +$ terraform import opc_compute_security_application.application1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_security_association.html.markdown b/website/source/docs/providers/opc/r/opc_compute_security_association.html.markdown index 0cbe442ef..2d711d589 100644 --- a/website/source/docs/providers/opc/r/opc_compute_security_association.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_security_association.html.markdown @@ -13,7 +13,7 @@ list in an OPC identity domain. ## Example Usage -``` +```hcl resource "opc_compute_security_association" "test_instance_sec_list_1" { name = "association1" vcable = "${opc_compute_instance.test_instance.vcable}" @@ -35,6 +35,6 @@ The following arguments are supported: Security Association's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_security_association.association1 example +```shell +$ terraform import opc_compute_security_association.association1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_security_ip_list.html.markdown b/website/source/docs/providers/opc/r/opc_compute_security_ip_list.html.markdown index 286ba5f9e..503c93efe 100644 --- a/website/source/docs/providers/opc/r/opc_compute_security_ip_list.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_security_ip_list.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_security_ip_list`` resource creates and manages a security IP ## Example Usage -``` +```hcl resource "opc_compute_security_ip_list" "sec_ip_list1" { name = "sec-ip-list1" ip_entries = ["217.138.34.4"] @@ -33,6 +33,6 @@ The following arguments are supported: IP List's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_ip_list.list1 example +```shell +$ terraform import opc_compute_ip_list.list1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_security_list.html.markdown b/website/source/docs/providers/opc/r/opc_compute_security_list.html.markdown index a7b84e692..461e6603e 100644 --- a/website/source/docs/providers/opc/r/opc_compute_security_list.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_security_list.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_security_list`` resource creates and manages a security list i ## Example Usage -``` +```hcl resource "opc_compute_security_list" "sec_list1" { name = "sec-list-1" policy = "permit" @@ -36,6 +36,6 @@ The following arguments are supported: Security List's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_security_list.list1 example +```shell +$ terraform import opc_compute_security_list.list1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_security_protocol.html.markdown b/website/source/docs/providers/opc/r/opc_compute_security_protocol.html.markdown index 46449a899..00208321a 100644 --- a/website/source/docs/providers/opc/r/opc_compute_security_protocol.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_security_protocol.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_security_protocol`` resource creates and manages a security pr ## Example Usage -``` +```hcl resource "opc_compute_security_protocol" "default" { name = "security-protocol-1" dst_ports = ["2045-2050"] @@ -60,6 +60,6 @@ In addition to the above, the following values are exported: ACL's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_security_protocol.default example +```shell +$ terraform import opc_compute_security_protocol.default example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_security_rule.html.markdown b/website/source/docs/providers/opc/r/opc_compute_security_rule.html.markdown index d2462980a..0a44150e7 100644 --- a/website/source/docs/providers/opc/r/opc_compute_security_rule.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_security_rule.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_security_rule`` resource creates and manages a security rule i ## Example Usage -``` +```hcl resource "opc_compute_security_rule" "default" { name = "SecurityRule1" flow_direction = "ingress" @@ -57,6 +57,6 @@ In addition to the above, the following attributes are exported: Security Rule's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_security_rule.rule1 example +```shell +$ terraform import opc_compute_security_rule.rule1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_ssh_key.html.markdown b/website/source/docs/providers/opc/r/opc_compute_ssh_key.html.markdown index 2969ebea2..e982b935a 100644 --- a/website/source/docs/providers/opc/r/opc_compute_ssh_key.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_ssh_key.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_ssh_key`` resource creates and manages an SSH key in an OPC id ## Example Usage -``` +```hcl resource "opc_compute_ssh_key" "%s" { name = "test-key" key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCqw6JwbjIk..." @@ -35,6 +35,6 @@ without removing it entirely from your Terraform resource definition. Defaults t SSH Key's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_ssh_key.key1 example +```shell +$ terraform import opc_compute_ssh_key.key1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_storage_volume.html.markdown b/website/source/docs/providers/opc/r/opc_compute_storage_volume.html.markdown index a71a5b249..cfa19a2ad 100644 --- a/website/source/docs/providers/opc/r/opc_compute_storage_volume.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_storage_volume.html.markdown @@ -14,7 +14,7 @@ The ``opc_compute_storage_volume`` resource creates and manages a storage volume ## Example Usage -``` +```hcl resource "opc_compute_storage_volume" "test" { name = "storageVolume1" description = "Description for the Storage Volume" @@ -24,7 +24,7 @@ resource "opc_compute_storage_volume" "test" { ``` ## Example Usage (Bootable Volume) -``` +```hcl resource "opc_compute_image_list" "test" { name = "imageList1" description = "Description for the Image List" @@ -77,6 +77,6 @@ The following attributes are exported: Storage Volume's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_storage_volume.volume1 example +```shell +$ terraform import opc_compute_storage_volume.volume1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_storage_volume_snapshot.html.markdown b/website/source/docs/providers/opc/r/opc_compute_storage_volume_snapshot.html.markdown index 7a167080c..885739762 100644 --- a/website/source/docs/providers/opc/r/opc_compute_storage_volume_snapshot.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_storage_volume_snapshot.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_storage_volume_snapshot`` resource creates and manages a stora ## Example Usage -``` +```hcl resource "opc_compute_storage_volume_snapshot" "test" { name = "storageVolume1" description = "Description for the Storage Volume" @@ -54,6 +54,6 @@ In addition to the attributes above, the following attributes are exported: Storage Volume Snapshot's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_storage_volume_snapshot.volume1 example +```shell +$ terraform import opc_compute_storage_volume_snapshot.volume1 example ``` diff --git a/website/source/docs/providers/opc/r/opc_compute_vnic_set.html.markdown b/website/source/docs/providers/opc/r/opc_compute_vnic_set.html.markdown index c5cf9c455..191ffa159 100644 --- a/website/source/docs/providers/opc/r/opc_compute_vnic_set.html.markdown +++ b/website/source/docs/providers/opc/r/opc_compute_vnic_set.html.markdown @@ -12,7 +12,7 @@ The ``opc_compute_vnic_set`` resource creates and manages a virtual NIC set in a ## Example Usage -``` +```hcl resource "opc_compute_vnic_set" "test_set" { name = "test_vnic_set" description = "My vnic set" @@ -40,6 +40,6 @@ The following arguments are supported: VNIC Set's can be imported using the `resource name`, e.g. -``` -terraform import opc_compute_vnic_set.set1 example +```shell +$ terraform import opc_compute_vnic_set.set1 example ``` From 6d3251b08d8fe246e3fb0cf108348024b228fe06 Mon Sep 17 00:00:00 2001 From: Dana Hoffman Date: Tue, 25 Apr 2017 13:03:36 -0700 Subject: [PATCH 36/89] provider/google: documentation and validation fixes for forwarding rules --- .../providers/google/resource_compute_forwarding_rule.go | 1 + .../google/r/compute_forwarding_rule.html.markdown | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/builtin/providers/google/resource_compute_forwarding_rule.go b/builtin/providers/google/resource_compute_forwarding_rule.go index b4bd4a779..e89e0cdce 100644 --- a/builtin/providers/google/resource_compute_forwarding_rule.go +++ b/builtin/providers/google/resource_compute_forwarding_rule.go @@ -89,6 +89,7 @@ func resourceComputeForwardingRule() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, Optional: true, Set: schema.HashString, + MaxItems: 5, }, "project": &schema.Schema{ diff --git a/website/source/docs/providers/google/r/compute_forwarding_rule.html.markdown b/website/source/docs/providers/google/r/compute_forwarding_rule.html.markdown index 845a7450b..a8c735187 100644 --- a/website/source/docs/providers/google/r/compute_forwarding_rule.html.markdown +++ b/website/source/docs/providers/google/r/compute_forwarding_rule.html.markdown @@ -3,7 +3,7 @@ layout: "google" page_title: "Google: google_compute_forwarding_rule" sidebar_current: "docs-google-compute-forwarding-rule" description: |- - Manages a Target Pool within GCE. + Manages a Forwarding Rule within GCE. --- # google\_compute\_forwarding\_rule @@ -54,8 +54,9 @@ The following arguments are supported: * `port_range` - (Optional) A range e.g. "1024-2048" or a single port "1024" (defaults to all ports!). Only used for external load balancing. -* `ports` - (Optional) A list of ports to use for internal load balancing - (defaults to all ports). +* `ports` - (Optional) A list of ports (maximum of 5) to use for internal load + balancing. Packets addressed to these ports will be forwarded to the backends + configured with this forwarding rule. Required for internal load balancing. * `project` - (Optional) The project in which the resource belongs. If it is not provided, the provider project is used. From 87db8b3b4312815d93a871c79a092b0fc54993fa Mon Sep 17 00:00:00 2001 From: ebilhoo Date: Tue, 25 Apr 2017 20:06:33 +0000 Subject: [PATCH 37/89] use validation helper --- .../ultradns/resource_ultradns_rdpool.go | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/builtin/providers/ultradns/resource_ultradns_rdpool.go b/builtin/providers/ultradns/resource_ultradns_rdpool.go index f18870a6c..5f8bbff6c 100644 --- a/builtin/providers/ultradns/resource_ultradns_rdpool.go +++ b/builtin/providers/ultradns/resource_ultradns_rdpool.go @@ -7,6 +7,7 @@ import ( "github.com/Ensighten/udnssdk" "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" ) func resourceUltradnsRdpool() *schema.Resource { @@ -39,28 +40,16 @@ func resourceUltradnsRdpool() *schema.Resource { Type: schema.TypeString, Optional: true, Default: "ROUND_ROBIN", - ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { - value := v.(string) - if value != "ROUND_ROBIN" && value != "FIXED" && value != "RANDOM" { - errors = append(errors, fmt.Errorf( - "Only 'ROUND_ROBIN', 'FIXED', and 'RANDOM' are supported values for 'order'")) - } - return - }, + ValidateFunc: validation.StringInSlice([]string{ + "ROUND_ROBIN", + "FIXED", + "RANDOM", + }, false), }, "description": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { - if v != nil { - value := v.(string) - if len(value) > 255 { - errors = append(errors, fmt.Errorf( - "'description' length must be 0-255")) - } - } - return - }, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 255), }, "ttl": &schema.Schema{ Type: schema.TypeInt, From e7c3575499e3931faa37367119ff9849978358c9 Mon Sep 17 00:00:00 2001 From: Dana Hoffman Date: Tue, 25 Apr 2017 13:20:02 -0700 Subject: [PATCH 38/89] provider/google: add attached_disk field to google_compute_instance (#13443) --- .../google/resource_compute_instance.go | 124 +++++++++++++++--- .../google/resource_compute_instance_test.go | 83 ++++++++++++ 2 files changed, 190 insertions(+), 17 deletions(-) diff --git a/builtin/providers/google/resource_compute_instance.go b/builtin/providers/google/resource_compute_instance.go index 46daaf315..c1bb4e77b 100644 --- a/builtin/providers/google/resource_compute_instance.go +++ b/builtin/providers/google/resource_compute_instance.go @@ -28,7 +28,7 @@ func resourceComputeInstance() *schema.Resource { Schema: map[string]*schema.Schema{ "disk": &schema.Schema{ Type: schema.TypeList, - Required: true, + Optional: true, ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -91,6 +91,40 @@ func resourceComputeInstance() *schema.Resource { }, }, + // Preferred way of adding persistent disks to an instance. + // Use this instead of `disk` when possible. + "attached_disk": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, // TODO(danawillow): Remove this, support attaching/detaching + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "device_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "disk_encryption_key_raw": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ForceNew: true, + }, + + "disk_encryption_key_sha256": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "machine_type": &schema.Schema{ Type: schema.TypeString, Required: true, @@ -371,7 +405,11 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err // Build up the list of disks disksCount := d.Get("disk.#").(int) - disks := make([]*compute.AttachedDisk, 0, disksCount) + attachedDisksCount := d.Get("attached_disk.#").(int) + if disksCount+attachedDisksCount == 0 { + return fmt.Errorf("At least one disk or attached_disk must be set") + } + disks := make([]*compute.AttachedDisk, 0, disksCount+attachedDisksCount) for i := 0; i < disksCount; i++ { prefix := fmt.Sprintf("disk.%d", i) @@ -457,6 +495,28 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err disks = append(disks, &disk) } + for i := 0; i < attachedDisksCount; i++ { + prefix := fmt.Sprintf("attached_disk.%d", i) + disk := compute.AttachedDisk{ + Source: d.Get(prefix + ".source").(string), + AutoDelete: false, // Don't allow autodelete; let terraform handle disk deletion + } + + disk.Boot = i == 0 && disksCount == 0 // TODO(danawillow): This is super hacky, let's just add a boot field. + + if v, ok := d.GetOk(prefix + ".device_name"); ok { + disk.DeviceName = v.(string) + } + + if v, ok := d.GetOk(prefix + ".disk_encryption_key_raw"); ok { + disk.DiskEncryptionKey = &compute.CustomerEncryptionKey{ + RawKey: v.(string), + } + } + + disks = append(disks, &disk) + } + networksCount := d.Get("network.#").(int) networkInterfacesCount := d.Get("network_interface.#").(int) @@ -791,24 +851,54 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error d.Set("tags_fingerprint", instance.Tags.Fingerprint) } - disks := make([]map[string]interface{}, 0, 1) - for i, disk := range instance.Disks { - di := map[string]interface{}{ - "disk": d.Get(fmt.Sprintf("disk.%d.disk", i)), - "image": d.Get(fmt.Sprintf("disk.%d.image", i)), - "type": d.Get(fmt.Sprintf("disk.%d.type", i)), - "scratch": d.Get(fmt.Sprintf("disk.%d.scratch", i)), - "auto_delete": d.Get(fmt.Sprintf("disk.%d.auto_delete", i)), - "size": d.Get(fmt.Sprintf("disk.%d.size", i)), - "device_name": d.Get(fmt.Sprintf("disk.%d.device_name", i)), - "disk_encryption_key_raw": d.Get(fmt.Sprintf("disk.%d.disk_encryption_key_raw", i)), + disksCount := d.Get("disk.#").(int) + attachedDisksCount := d.Get("attached_disk.#").(int) + disks := make([]map[string]interface{}, 0, disksCount) + attachedDisks := make([]map[string]interface{}, 0, attachedDisksCount) + + if expectedDisks := disksCount + attachedDisksCount; len(instance.Disks) != expectedDisks { + return fmt.Errorf("Expected %d disks, API returned %d", expectedDisks, len(instance.Disks)) + } + + attachedDiskSources := make(map[string]struct{}, attachedDisksCount) + for i := 0; i < attachedDisksCount; i++ { + attachedDiskSources[d.Get(fmt.Sprintf("attached_disk.%d.source", i)).(string)] = struct{}{} + } + + dIndex := 0 + adIndex := 0 + for _, disk := range instance.Disks { + if _, ok := attachedDiskSources[disk.Source]; !ok { + di := map[string]interface{}{ + "disk": d.Get(fmt.Sprintf("disk.%d.disk", dIndex)), + "image": d.Get(fmt.Sprintf("disk.%d.image", dIndex)), + "type": d.Get(fmt.Sprintf("disk.%d.type", dIndex)), + "scratch": d.Get(fmt.Sprintf("disk.%d.scratch", dIndex)), + "auto_delete": d.Get(fmt.Sprintf("disk.%d.auto_delete", dIndex)), + "size": d.Get(fmt.Sprintf("disk.%d.size", dIndex)), + "device_name": d.Get(fmt.Sprintf("disk.%d.device_name", dIndex)), + "disk_encryption_key_raw": d.Get(fmt.Sprintf("disk.%d.disk_encryption_key_raw", dIndex)), + } + if disk.DiskEncryptionKey != nil && disk.DiskEncryptionKey.Sha256 != "" { + di["disk_encryption_key_sha256"] = disk.DiskEncryptionKey.Sha256 + } + disks = append(disks, di) + dIndex++ + } else { + di := map[string]interface{}{ + "source": disk.Source, + "device_name": disk.DeviceName, + "disk_encryption_key_raw": d.Get(fmt.Sprintf("attached_disk.%d.disk_encryption_key_raw", adIndex)), + } + if disk.DiskEncryptionKey != nil && disk.DiskEncryptionKey.Sha256 != "" { + di["disk_encryption_key_sha256"] = disk.DiskEncryptionKey.Sha256 + } + attachedDisks = append(attachedDisks, di) + adIndex++ } - if disk.DiskEncryptionKey != nil && disk.DiskEncryptionKey.Sha256 != "" { - di["disk_encryption_key_sha256"] = disk.DiskEncryptionKey.Sha256 - } - disks = append(disks, di) } d.Set("disk", disks) + d.Set("attached_disk", attachedDisks) d.Set("self_link", instance.SelfLink) d.SetId(instance.Name) diff --git a/builtin/providers/google/resource_compute_instance_test.go b/builtin/providers/google/resource_compute_instance_test.go index a4d52d872..e91368e24 100644 --- a/builtin/providers/google/resource_compute_instance_test.go +++ b/builtin/providers/google/resource_compute_instance_test.go @@ -244,6 +244,44 @@ func TestAccComputeInstance_diskEncryption(t *testing.T) { }) } +func TestAccComputeInstance_attachedDisk(t *testing.T) { + var instance compute.Instance + var instanceName = fmt.Sprintf("instance-test-%s", acctest.RandString(10)) + var diskName = fmt.Sprintf("instance-testd-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstance_attachedDisk(diskName, instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceDisk(&instance, diskName, false, true), + ), + }, + }, + }) +} + +func TestAccComputeInstance_noDisk(t *testing.T) { + var instanceName = fmt.Sprintf("instance-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstance_noDisk(instanceName), + ExpectError: regexp.MustCompile("At least one disk or attached_disk must be set"), + }, + }, + }) +} + func TestAccComputeInstance_local_ssd(t *testing.T) { var instance compute.Instance var instanceName = fmt.Sprintf("instance-test-%s", acctest.RandString(10)) @@ -1121,6 +1159,51 @@ func testAccComputeInstance_disks_encryption(disk, instance string) string { }`, disk, instance) } +func testAccComputeInstance_attachedDisk(disk, instance string) string { + return fmt.Sprintf(` + resource "google_compute_disk" "foobar" { + name = "%s" + size = 10 + type = "pd-ssd" + zone = "us-central1-a" + } + + resource "google_compute_instance" "foobar" { + name = "%s" + machine_type = "n1-standard-1" + zone = "us-central1-a" + + attached_disk { + source = "${google_compute_disk.foobar.self_link}" + } + + network_interface { + network = "default" + } + + metadata { + foo = "bar" + } + }`, disk, instance) +} + +func testAccComputeInstance_noDisk(instance string) string { + return fmt.Sprintf(` + resource "google_compute_instance" "foobar" { + name = "%s" + machine_type = "n1-standard-1" + zone = "us-central1-a" + + network_interface { + network = "default" + } + + metadata { + foo = "bar" + } + }`, instance) +} + func testAccComputeInstance_local_ssd(instance string) string { return fmt.Sprintf(` resource "google_compute_instance" "local-ssd" { From fbd7c4ef929cf7a0968fc89e988766bb6fe37af0 Mon Sep 17 00:00:00 2001 From: Dana Hoffman Date: Tue, 25 Apr 2017 13:21:37 -0700 Subject: [PATCH 39/89] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 903bcf1bb..4ed48b25d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ IMPROVEMENTS: * provider/fastly: Add support for GCS logging [GH-13553] * provider/google: `google_compute_address` and `google_compute_global_address` are now importable [GH-13270] * provider/google: `google_compute_network` is now importable [GH-13834] + * provider/google: add attached_disk field to google_compute_instance [GH-13443] * provider/heroku: Set App buildpacks from config [GH-13910] * provider/vault: `vault_generic_secret` resource can now optionally detect drift if it has appropriate access [GH-11776] From eb374b795b4c5eb4c41443f935443f1ed2a21d13 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 25 Apr 2017 21:25:36 +0100 Subject: [PATCH 40/89] provider/aws: Add test for SNS subscription w/ autoconfirming endpoint (#13912) --- ...esource_aws_sns_topic_subscription_test.go | 142 ++++++++++++++++++ .../aws/test-fixtures/lambda_confirm_sns.zip | Bin 0 -> 565 bytes 2 files changed, 142 insertions(+) create mode 100644 builtin/providers/aws/test-fixtures/lambda_confirm_sns.zip diff --git a/builtin/providers/aws/resource_aws_sns_topic_subscription_test.go b/builtin/providers/aws/resource_aws_sns_topic_subscription_test.go index 146d2fa92..3f730c9f7 100644 --- a/builtin/providers/aws/resource_aws_sns_topic_subscription_test.go +++ b/builtin/providers/aws/resource_aws_sns_topic_subscription_test.go @@ -31,6 +31,25 @@ func TestAccAWSSNSTopicSubscription_basic(t *testing.T) { }) } +func TestAccAWSSNSTopicSubscription_autoConfirmingEndpoint(t *testing.T) { + ri := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSNSTopicSubscriptionDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSSNSTopicSubscriptionConfig_autoConfirmingEndpoint(ri), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic"), + testAccCheckAWSSNSTopicSubscriptionExists("aws_sns_topic_subscription.test_subscription"), + ), + }, + }, + }) +} + func testAccCheckAWSSNSTopicSubscriptionDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).snsconn @@ -103,3 +122,126 @@ resource "aws_sns_topic_subscription" "test_subscription" { } `, i) } + +func testAccAWSSNSTopicSubscriptionConfig_autoConfirmingEndpoint(i int) string { + return fmt.Sprintf(` +resource "aws_sns_topic" "test_topic" { + name = "tf-acc-test-sns-%d" +} + +resource "aws_api_gateway_rest_api" "test" { + name = "tf-acc-test-sns-%d" + description = "Terraform Acceptance test for SNS subscription" +} + +resource "aws_api_gateway_method" "test" { + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + resource_id = "${aws_api_gateway_rest_api.test.root_resource_id}" + http_method = "POST" + authorization = "NONE" +} + +resource "aws_api_gateway_method_response" "test" { + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + resource_id = "${aws_api_gateway_rest_api.test.root_resource_id}" + http_method = "${aws_api_gateway_method.test.http_method}" + status_code = "200" + + response_parameters { + "method.response.header.Access-Control-Allow-Origin" = true + } +} + +resource "aws_api_gateway_integration" "test" { + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + resource_id = "${aws_api_gateway_rest_api.test.root_resource_id}" + http_method = "${aws_api_gateway_method.test.http_method}" + integration_http_method = "POST" + type = "AWS" + uri = "${aws_lambda_function.lambda.invoke_arn}" +} + +resource "aws_api_gateway_integration_response" "test" { + depends_on = ["aws_api_gateway_integration.test"] + rest_api_id = "${aws_api_gateway_rest_api.test.id}" + resource_id = "${aws_api_gateway_rest_api.test.root_resource_id}" + http_method = "${aws_api_gateway_method.test.http_method}" + status_code = "${aws_api_gateway_method_response.test.status_code}" + + response_parameters { + "method.response.header.Access-Control-Allow-Origin" = "'*'" + } +} + +resource "aws_iam_role" "iam_for_lambda" { + name = "tf-acc-test-sns-%d" + + assume_role_policy = <5~>*xk~%_?Q5AT&!=f{uDf-pq%-iSx3%i=>(8riioV>m^0J)VnuBFx z@`nN@H_m^{D*w`1EUP|O^pxd^)0>SgrNy6wtyTV_tiAuqBxxn@0;Z|o=J85+-gslO zKY7^%oA3P%YQ7c-pu)0`3WkTxZdj}^zG+?d1 z_dVEa<6`-a`>ea(nl0PjeKc6w<5}W_Quh-E3XlJC3;9IW?+>W9-ng0n`Qq9ew_A@} zTQW=j?Y94D?eQYv(|^ysk8gLx2Y53w$uZ-KQwdO Date: Tue, 25 Apr 2017 17:18:22 -0400 Subject: [PATCH 41/89] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ed48b25d..7c18da073 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ BUG FIXES: * provider/fastly: Add ability to associate a healthcheck to a backend [GH-13539] * provider/google: Stop setting the id when project creation fails [GH-13644] * provider/google: Make ports in resource_compute_forwarding_rule ForceNew [GH-13833] + * provider/ignition: Internal cache moved to global, instead per provider instance [GH-13919] * provider/logentries: Refresh from state when resources not found [GH-13810] * provider/newrelic: newrelic_alert_condition - `condition_scope` must be `application` or `instance` [GH-12972] * provider/opc: fixed issue with unqualifying nats [GH-13826] From 048bfd6f36e01e2e3f02b010c4fd8d7236f4f555 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Tue, 25 Apr 2017 17:35:59 -0400 Subject: [PATCH 42/89] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c18da073..6d3404574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ IMPROVEMENTS: * provider/aws: Add tagging support to the 'aws_lambda_function' resource [GH-13873] * provider/aws: Validate WAF metric names [GH-13885] * provider/aws: Allow AWS Subnet to change IPv6 CIDR Block without ForceNew [GH-13909] + * provider/aws: Allow filtering of aws_subnet_ids by tags [GH-13937] * provider/azurerm: VM Scale Sets - import support [GH-13464] * provider/azurerm: Allow Azure China region support [GH-13767] * provider/digitalocean: Export droplet prices [GH-13720] From 9f99fc46cef34cd4a305858449f7ca00421dd0e4 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Tue, 25 Apr 2017 18:02:43 -0400 Subject: [PATCH 43/89] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d3404574..e5584ed9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ IMPROVEMENTS: * provider/google: `google_compute_network` is now importable [GH-13834] * provider/google: add attached_disk field to google_compute_instance [GH-13443] * provider/heroku: Set App buildpacks from config [GH-13910] + * provider/heroku: Create Heroku app in a private space [GH-13862] * provider/vault: `vault_generic_secret` resource can now optionally detect drift if it has appropriate access [GH-11776] BUG FIXES: From d721ff6d66e2377f76f9b61f1133769386311eb6 Mon Sep 17 00:00:00 2001 From: Joshua Spence Date: Wed, 26 Apr 2017 08:11:21 +1000 Subject: [PATCH 44/89] provider/aws: Sort AMI and snapshot IDs (#13866) As a follow up to #13844, this pull request sorts the AMIs and snapshots returned from the aws_ami_ids and aws_ebs_snapshot_ids data sources, respectively. --- builtin/providers/aws/data_source_aws_ami.go | 16 +--- .../providers/aws/data_source_aws_ami_ids.go | 5 +- .../aws/data_source_aws_ami_ids_test.go | 80 +++++++++++++++++-- .../aws/data_source_aws_ebs_snapshot.go | 15 +--- .../aws/data_source_aws_ebs_snapshot_ids.go | 5 +- .../data_source_aws_ebs_snapshot_ids_test.go | 74 ++++++++++++++++- builtin/providers/aws/sort.go | 53 ++++++++++++ .../providers/aws/d/ami_ids.html.markdown | 3 +- .../aws/d/ebs_snapshot_ids.html.markdown | 3 +- 9 files changed, 211 insertions(+), 43 deletions(-) create mode 100644 builtin/providers/aws/sort.go diff --git a/builtin/providers/aws/data_source_aws_ami.go b/builtin/providers/aws/data_source_aws_ami.go index 877069a22..05513c32e 100644 --- a/builtin/providers/aws/data_source_aws_ami.go +++ b/builtin/providers/aws/data_source_aws_ami.go @@ -5,8 +5,6 @@ import ( "fmt" "log" "regexp" - "sort" - "time" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/hashcode" @@ -249,21 +247,9 @@ func dataSourceAwsAmiRead(d *schema.ResourceData, meta interface{}) error { return amiDescriptionAttributes(d, image) } -type imageSort []*ec2.Image - -func (a imageSort) Len() int { return len(a) } -func (a imageSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a imageSort) Less(i, j int) bool { - itime, _ := time.Parse(time.RFC3339, *a[i].CreationDate) - jtime, _ := time.Parse(time.RFC3339, *a[j].CreationDate) - return itime.Unix() < jtime.Unix() -} - // Returns the most recent AMI out of a slice of images. func mostRecentAmi(images []*ec2.Image) *ec2.Image { - sortedImages := images - sort.Sort(imageSort(sortedImages)) - return sortedImages[len(sortedImages)-1] + return sortImages(images)[0] } // populate the numerous fields that the image description returns. diff --git a/builtin/providers/aws/data_source_aws_ami_ids.go b/builtin/providers/aws/data_source_aws_ami_ids.go index bbf4438d5..20df34ac3 100644 --- a/builtin/providers/aws/data_source_aws_ami_ids.go +++ b/builtin/providers/aws/data_source_aws_ami_ids.go @@ -36,10 +36,9 @@ func dataSourceAwsAmiIds() *schema.Resource { }, "tags": dataSourceTagsSchema(), "ids": &schema.Schema{ - Type: schema.TypeSet, + Type: schema.TypeList, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, }, }, } @@ -101,7 +100,7 @@ func dataSourceAwsAmiIdsRead(d *schema.ResourceData, meta interface{}) error { filteredImages = resp.Images[:] } - for _, image := range filteredImages { + for _, image := range sortImages(filteredImages) { imageIds = append(imageIds, *image.ImageId) } diff --git a/builtin/providers/aws/data_source_aws_ami_ids_test.go b/builtin/providers/aws/data_source_aws_ami_ids_test.go index e2a7ac2d8..52582eaba 100644 --- a/builtin/providers/aws/data_source_aws_ami_ids_test.go +++ b/builtin/providers/aws/data_source_aws_ami_ids_test.go @@ -1,9 +1,11 @@ package aws import ( + "fmt" "testing" "github.com/hashicorp/terraform/helper/resource" + "github.com/satori/uuid" ) func TestAccDataSourceAwsAmiIds_basic(t *testing.T) { @@ -21,6 +23,37 @@ func TestAccDataSourceAwsAmiIds_basic(t *testing.T) { }) } +func TestAccDataSourceAwsAmiIds_sorted(t *testing.T) { + uuid := uuid.NewV4().String() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsAmiIdsConfig_sorted1(uuid), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("aws_ami_from_instance.a", "id"), + resource.TestCheckResourceAttrSet("aws_ami_from_instance.b", "id"), + ), + }, + { + Config: testAccDataSourceAwsAmiIdsConfig_sorted2(uuid), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsEbsSnapshotDataSourceID("data.aws_ami_ids.test"), + resource.TestCheckResourceAttr("data.aws_ami_ids.test", "ids.#", "2"), + resource.TestCheckResourceAttrPair( + "data.aws_ami_ids.test", "ids.0", + "aws_ami_from_instance.b", "id"), + resource.TestCheckResourceAttrPair( + "data.aws_ami_ids.test", "ids.1", + "aws_ami_from_instance.a", "id"), + ), + }, + }, + }) +} + func TestAccDataSourceAwsAmiIds_empty(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -39,15 +72,52 @@ func TestAccDataSourceAwsAmiIds_empty(t *testing.T) { const testAccDataSourceAwsAmiIdsConfig_basic = ` data "aws_ami_ids" "ubuntu" { - owners = ["099720109477"] + owners = ["099720109477"] - filter { - name = "name" - values = ["ubuntu/images/ubuntu-*-*-amd64-server-*"] - } + filter { + name = "name" + values = ["ubuntu/images/ubuntu-*-*-amd64-server-*"] + } } ` +func testAccDataSourceAwsAmiIdsConfig_sorted1(uuid string) string { + return fmt.Sprintf(` +resource "aws_instance" "test" { + ami = "ami-efd0428f" + instance_type = "m3.medium" + + count = 2 +} + +resource "aws_ami_from_instance" "a" { + name = "tf-test-%s-a" + source_instance_id = "${aws_instance.test.*.id[0]}" + snapshot_without_reboot = true +} + +resource "aws_ami_from_instance" "b" { + name = "tf-test-%s-b" + source_instance_id = "${aws_instance.test.*.id[1]}" + snapshot_without_reboot = true + + // We want to ensure that 'aws_ami_from_instance.a.creation_date' is less + // than 'aws_ami_from_instance.b.creation_date' so that we can ensure that + // the images are being sorted correctly. + depends_on = ["aws_ami_from_instance.a"] +} +`, uuid, uuid) +} + +func testAccDataSourceAwsAmiIdsConfig_sorted2(uuid string) string { + return testAccDataSourceAwsAmiIdsConfig_sorted1(uuid) + fmt.Sprintf(` +data "aws_ami_ids" "test" { + owners = ["self"] + name_regex = "^tf-test-%s-" +} +`, uuid) +} + const testAccDataSourceAwsAmiIdsConfig_empty = ` data "aws_ami_ids" "empty" { filter { diff --git a/builtin/providers/aws/data_source_aws_ebs_snapshot.go b/builtin/providers/aws/data_source_aws_ebs_snapshot.go index b16f61f22..a36334723 100644 --- a/builtin/providers/aws/data_source_aws_ebs_snapshot.go +++ b/builtin/providers/aws/data_source_aws_ebs_snapshot.go @@ -3,7 +3,6 @@ package aws import ( "fmt" "log" - "sort" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/schema" @@ -138,20 +137,8 @@ func dataSourceAwsEbsSnapshotRead(d *schema.ResourceData, meta interface{}) erro return snapshotDescriptionAttributes(d, snapshot) } -type snapshotSort []*ec2.Snapshot - -func (a snapshotSort) Len() int { return len(a) } -func (a snapshotSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a snapshotSort) Less(i, j int) bool { - itime := *a[i].StartTime - jtime := *a[j].StartTime - return itime.Unix() < jtime.Unix() -} - func mostRecentSnapshot(snapshots []*ec2.Snapshot) *ec2.Snapshot { - sortedSnapshots := snapshots - sort.Sort(snapshotSort(sortedSnapshots)) - return sortedSnapshots[len(sortedSnapshots)-1] + return sortSnapshots(snapshots)[0] } func snapshotDescriptionAttributes(d *schema.ResourceData, snapshot *ec2.Snapshot) error { diff --git a/builtin/providers/aws/data_source_aws_ebs_snapshot_ids.go b/builtin/providers/aws/data_source_aws_ebs_snapshot_ids.go index 57dc20e9c..bd4f2ad8b 100644 --- a/builtin/providers/aws/data_source_aws_ebs_snapshot_ids.go +++ b/builtin/providers/aws/data_source_aws_ebs_snapshot_ids.go @@ -28,10 +28,9 @@ func dataSourceAwsEbsSnapshotIds() *schema.Resource { }, "tags": dataSourceTagsSchema(), "ids": &schema.Schema{ - Type: schema.TypeSet, + Type: schema.TypeList, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, }, }, } @@ -67,7 +66,7 @@ func dataSourceAwsEbsSnapshotIdsRead(d *schema.ResourceData, meta interface{}) e snapshotIds := make([]string, 0) - for _, snapshot := range resp.Snapshots { + for _, snapshot := range sortSnapshots(resp.Snapshots) { snapshotIds = append(snapshotIds, *snapshot.SnapshotId) } diff --git a/builtin/providers/aws/data_source_aws_ebs_snapshot_ids_test.go b/builtin/providers/aws/data_source_aws_ebs_snapshot_ids_test.go index 869152ac4..0c5f3ec4d 100644 --- a/builtin/providers/aws/data_source_aws_ebs_snapshot_ids_test.go +++ b/builtin/providers/aws/data_source_aws_ebs_snapshot_ids_test.go @@ -1,9 +1,11 @@ package aws import ( + "fmt" "testing" "github.com/hashicorp/terraform/helper/resource" + "github.com/satori/uuid" ) func TestAccDataSourceAwsEbsSnapshotIds_basic(t *testing.T) { @@ -21,6 +23,37 @@ func TestAccDataSourceAwsEbsSnapshotIds_basic(t *testing.T) { }) } +func TestAccDataSourceAwsEbsSnapshotIds_sorted(t *testing.T) { + uuid := uuid.NewV4().String() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsEbsSnapshotIdsConfig_sorted1(uuid), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("aws_ebs_snapshot.a", "id"), + resource.TestCheckResourceAttrSet("aws_ebs_snapshot.b", "id"), + ), + }, + { + Config: testAccDataSourceAwsEbsSnapshotIdsConfig_sorted2(uuid), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsEbsSnapshotDataSourceID("data.aws_ebs_snapshot_ids.test"), + resource.TestCheckResourceAttr("data.aws_ebs_snapshot_ids.test", "ids.#", "2"), + resource.TestCheckResourceAttrPair( + "data.aws_ebs_snapshot_ids.test", "ids.0", + "aws_ebs_snapshot.b", "id"), + resource.TestCheckResourceAttrPair( + "data.aws_ebs_snapshot_ids.test", "ids.1", + "aws_ebs_snapshot.a", "id"), + ), + }, + }, + }) +} + func TestAccDataSourceAwsEbsSnapshotIds_empty(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -40,7 +73,7 @@ func TestAccDataSourceAwsEbsSnapshotIds_empty(t *testing.T) { const testAccDataSourceAwsEbsSnapshotIdsConfig_basic = ` resource "aws_ebs_volume" "test" { availability_zone = "us-west-2a" - size = 40 + size = 1 } resource "aws_ebs_snapshot" "test" { @@ -52,6 +85,45 @@ data "aws_ebs_snapshot_ids" "test" { } ` +func testAccDataSourceAwsEbsSnapshotIdsConfig_sorted1(uuid string) string { + return fmt.Sprintf(` +resource "aws_ebs_volume" "test" { + availability_zone = "us-west-2a" + size = 1 + + count = 2 +} + +resource "aws_ebs_snapshot" "a" { + volume_id = "${aws_ebs_volume.test.*.id[0]}" + description = "tf-test-%s" +} + +resource "aws_ebs_snapshot" "b" { + volume_id = "${aws_ebs_volume.test.*.id[1]}" + description = "tf-test-%s" + + // We want to ensure that 'aws_ebs_snapshot.a.creation_date' is less than + // 'aws_ebs_snapshot.b.creation_date'/ so that we can ensure that the + // snapshots are being sorted correctly. + depends_on = ["aws_ebs_snapshot.a"] +} +`, uuid, uuid) +} + +func testAccDataSourceAwsEbsSnapshotIdsConfig_sorted2(uuid string) string { + return testAccDataSourceAwsEbsSnapshotIdsConfig_sorted1(uuid) + fmt.Sprintf(` +data "aws_ebs_snapshot_ids" "test" { + owners = ["self"] + + filter { + name = "description" + values = ["tf-test-%s"] + } +} +`, uuid) +} + const testAccDataSourceAwsEbsSnapshotIdsConfig_empty = ` data "aws_ebs_snapshot_ids" "empty" { owners = ["000000000000"] diff --git a/builtin/providers/aws/sort.go b/builtin/providers/aws/sort.go new file mode 100644 index 000000000..0d90458eb --- /dev/null +++ b/builtin/providers/aws/sort.go @@ -0,0 +1,53 @@ +package aws + +import ( + "sort" + "time" + + "github.com/aws/aws-sdk-go/service/ec2" +) + +type imageSort []*ec2.Image +type snapshotSort []*ec2.Snapshot + +func (a imageSort) Len() int { + return len(a) +} + +func (a imageSort) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a imageSort) Less(i, j int) bool { + itime, _ := time.Parse(time.RFC3339, *a[i].CreationDate) + jtime, _ := time.Parse(time.RFC3339, *a[j].CreationDate) + return itime.Unix() < jtime.Unix() +} + +// Sort images by creation date, in descending order. +func sortImages(images []*ec2.Image) []*ec2.Image { + sortedImages := images + sort.Sort(sort.Reverse(imageSort(sortedImages))) + return sortedImages +} + +func (a snapshotSort) Len() int { + return len(a) +} + +func (a snapshotSort) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a snapshotSort) Less(i, j int) bool { + itime := *a[i].StartTime + jtime := *a[j].StartTime + return itime.Unix() < jtime.Unix() +} + +// Sort snapshots by creation date, in descending order. +func sortSnapshots(snapshots []*ec2.Snapshot) []*ec2.Snapshot { + sortedSnapshots := snapshots + sort.Sort(sort.Reverse(snapshotSort(sortedSnapshots))) + return sortedSnapshots +} diff --git a/website/source/docs/providers/aws/d/ami_ids.html.markdown b/website/source/docs/providers/aws/d/ami_ids.html.markdown index 526977bdc..8bad15bee 100644 --- a/website/source/docs/providers/aws/d/ami_ids.html.markdown +++ b/website/source/docs/providers/aws/d/ami_ids.html.markdown @@ -46,6 +46,7 @@ options to narrow down the list AWS returns. ## Attributes Reference -`ids` is set to the list of AMI IDs. +`ids` is set to the list of AMI IDs, sorted by creation time in descending +order. [1]: http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-images.html diff --git a/website/source/docs/providers/aws/d/ebs_snapshot_ids.html.markdown b/website/source/docs/providers/aws/d/ebs_snapshot_ids.html.markdown index 6d4ef617d..dfe472b5f 100644 --- a/website/source/docs/providers/aws/d/ebs_snapshot_ids.html.markdown +++ b/website/source/docs/providers/aws/d/ebs_snapshot_ids.html.markdown @@ -43,6 +43,7 @@ several valid keys, for a full reference, check out ## Attributes Reference -`ids` is set to the list of EBS snapshot IDs. +`ids` is set to the list of EBS snapshot IDs, sorted by creation time in +descending order. [1]: http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-snapshots.html From 0e0a5150ff02b21b7107f1529794e66f8248e372 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 25 Apr 2017 15:12:14 -0700 Subject: [PATCH 45/89] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5584ed9f..f971216af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,8 @@ FEATURES: * **New Provider:** `opc` - Oracle Public Cloud [GH-13468] * **New Provider:** `oneandone` [GH-13633] -* **New Data Source:** `aws_ami_ids` [GH-13844] -* **New Data Source:** `aws_ebs_snapshot_ids` [GH-13844] +* **New Data Source:** `aws_ami_ids` [GH-13844] [GH-13866] +* **New Data Source:** `aws_ebs_snapshot_ids` [GH-13844] [GH-13866] * **New Data Source:** `aws_kms_alias` [GH-13669] * **New Data Source:** `aws_kinesis_stream` [GH-13562] * **New Data Source:** `digitalocean_image` [GH-13787] From f4015b43c55d5a9db5048ccf74feeb2b0fae7d03 Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Wed, 26 Apr 2017 10:12:38 +1200 Subject: [PATCH 46/89] provider/aws: Support aws_instance and volume tagging on creation (#13945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: #13173 We now tag at instance creation and introduced `volume_tags` that can be set so that all devices created on instance creation will receive those tags ``` % make testacc TEST=./builtin/providers/aws TESTARGS='-run=TestAccAWSInstance_volumeTags' 2 ↵ ✚ ✭ ==> Checking that code complies with gofmt requirements... go generate $(go list ./... | grep -v /terraform/vendor/) 2017/04/26 06:30:48 Generated command/internal_plugin_list.go TF_ACC=1 go test ./builtin/providers/aws -v -run=TestAccAWSInstance_volumeTags -timeout 120m === RUN TestAccAWSInstance_volumeTags --- PASS: TestAccAWSInstance_volumeTags (214.31s) PASS ok github.com/hashicorp/terraform/builtin/providers/aws 214.332s ``` --- .../providers/aws/resource_aws_instance.go | 112 +++++++++++- .../aws/resource_aws_instance_migrate.go | 4 +- .../aws/resource_aws_instance_test.go | 162 +++++++++++++++++- builtin/providers/aws/tags.go | 57 ++++++ .../providers/aws/r/instance.html.markdown | 1 + 5 files changed, 328 insertions(+), 8 deletions(-) diff --git a/builtin/providers/aws/resource_aws_instance.go b/builtin/providers/aws/resource_aws_instance.go index 65e348d34..4e51e6869 100644 --- a/builtin/providers/aws/resource_aws_instance.go +++ b/builtin/providers/aws/resource_aws_instance.go @@ -200,6 +200,8 @@ func resourceAwsInstance() *schema.Resource { "tags": tagsSchema(), + "volume_tags": tagsSchema(), + "block_device": { Type: schema.TypeMap, Optional: true, @@ -396,6 +398,34 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { runOpts.Ipv6Addresses = ipv6Addresses } + tagsSpec := make([]*ec2.TagSpecification, 0) + + if v, ok := d.GetOk("tags"); ok { + tags := tagsFromMap(v.(map[string]interface{})) + + spec := &ec2.TagSpecification{ + ResourceType: aws.String("instance"), + Tags: tags, + } + + tagsSpec = append(tagsSpec, spec) + } + + if v, ok := d.GetOk("volume_tags"); ok { + tags := tagsFromMap(v.(map[string]interface{})) + + spec := &ec2.TagSpecification{ + ResourceType: aws.String("volume"), + Tags: tags, + } + + tagsSpec = append(tagsSpec, spec) + } + + if len(tagsSpec) > 0 { + runOpts.TagSpecifications = tagsSpec + } + // Create the instance log.Printf("[DEBUG] Run configuration: %s", runOpts) @@ -563,6 +593,10 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { d.Set("tags", tagsToMap(instance.Tags)) + if err := readVolumeTags(conn, d); err != nil { + return err + } + if err := readSecurityGroups(d, instance); err != nil { return err } @@ -605,16 +639,27 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ec2conn d.Partial(true) - if err := setTags(conn, d); err != nil { - return err - } else { - d.SetPartial("tags") + + if d.HasChange("tags") && !d.IsNewResource() { + if err := setTags(conn, d); err != nil { + return err + } else { + d.SetPartial("tags") + } + } + + if d.HasChange("volume_tags") && !d.IsNewResource() { + if err := setVolumeTags(conn, d); err != nil { + return err + } else { + d.SetPartial("volume_tags") + } } if d.HasChange("iam_instance_profile") && !d.IsNewResource() { request := &ec2.DescribeIamInstanceProfileAssociationsInput{ Filters: []*ec2.Filter{ - &ec2.Filter{ + { Name: aws.String("instance-id"), Values: []*string{aws.String(d.Id())}, }, @@ -1125,6 +1170,39 @@ func readBlockDeviceMappingsFromConfig( return blockDevices, nil } +func readVolumeTags(conn *ec2.EC2, d *schema.ResourceData) error { + volumeIds, err := getAwsInstanceVolumeIds(conn, d) + if err != nil { + return err + } + + tagsResp, err := conn.DescribeTags(&ec2.DescribeTagsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("resource-id"), + Values: volumeIds, + }, + }, + }) + if err != nil { + return err + } + + var tags []*ec2.Tag + + for _, t := range tagsResp.Tags { + tag := &ec2.Tag{ + Key: t.Key, + Value: t.Value, + } + tags = append(tags, tag) + } + + d.Set("volume_tags", tagsToMap(tags)) + + return nil +} + // Determine whether we're referring to security groups with // IDs or names. We use a heuristic to figure this out. By default, // we use IDs if we're in a VPC. However, if we previously had an @@ -1372,3 +1450,27 @@ func userDataHashSum(user_data string) string { hash := sha1.Sum(v) return hex.EncodeToString(hash[:]) } + +func getAwsInstanceVolumeIds(conn *ec2.EC2, d *schema.ResourceData) ([]*string, error) { + volumeIds := make([]*string, 0) + + opts := &ec2.DescribeVolumesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("attachment.instance-id"), + Values: []*string{aws.String(d.Id())}, + }, + }, + } + + resp, err := conn.DescribeVolumes(opts) + if err != nil { + return nil, err + } + + for _, v := range resp.Volumes { + volumeIds = append(volumeIds, v.VolumeId) + } + + return volumeIds, nil +} diff --git a/builtin/providers/aws/resource_aws_instance_migrate.go b/builtin/providers/aws/resource_aws_instance_migrate.go index 28a256b7b..31f28b39f 100644 --- a/builtin/providers/aws/resource_aws_instance_migrate.go +++ b/builtin/providers/aws/resource_aws_instance_migrate.go @@ -15,13 +15,13 @@ func resourceAwsInstanceMigrateState( switch v { case 0: log.Println("[INFO] Found AWS Instance State v0; migrating to v1") - return migrateStateV0toV1(is) + return migrateAwsInstanceStateV0toV1(is) default: return is, fmt.Errorf("Unexpected schema version: %d", v) } } -func migrateStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { +func migrateAwsInstanceStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { if is.Empty() || is.Attributes == nil { log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") return is, nil diff --git a/builtin/providers/aws/resource_aws_instance_test.go b/builtin/providers/aws/resource_aws_instance_test.go index 2b835f6d7..3426c35d9 100644 --- a/builtin/providers/aws/resource_aws_instance_test.go +++ b/builtin/providers/aws/resource_aws_instance_test.go @@ -616,7 +616,6 @@ func TestAccAWSInstance_tags(t *testing.T) { testAccCheckTags(&v.Tags, "#", ""), ), }, - { Config: testAccCheckInstanceConfigTagsUpdate, Check: resource.ComposeTestCheckFunc( @@ -629,6 +628,56 @@ func TestAccAWSInstance_tags(t *testing.T) { }) } +func TestAccAWSInstance_volumeTags(t *testing.T) { + var v ec2.Instance + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckInstanceConfigNoVolumeTags, + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists("aws_instance.foo", &v), + resource.TestCheckNoResourceAttr( + "aws_instance.foo", "volume_tags"), + ), + }, + { + Config: testAccCheckInstanceConfigWithVolumeTags, + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists("aws_instance.foo", &v), + resource.TestCheckResourceAttr( + "aws_instance.foo", "volume_tags.%", "1"), + resource.TestCheckResourceAttr( + "aws_instance.foo", "volume_tags.Name", "acceptance-test-volume-tag"), + ), + }, + { + Config: testAccCheckInstanceConfigWithVolumeTagsUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists("aws_instance.foo", &v), + resource.TestCheckResourceAttr( + "aws_instance.foo", "volume_tags.%", "2"), + resource.TestCheckResourceAttr( + "aws_instance.foo", "volume_tags.Name", "acceptance-test-volume-tag"), + resource.TestCheckResourceAttr( + "aws_instance.foo", "volume_tags.Environment", "dev"), + ), + }, + { + Config: testAccCheckInstanceConfigNoVolumeTags, + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists("aws_instance.foo", &v), + resource.TestCheckNoResourceAttr( + "aws_instance.foo", "volume_tags"), + ), + }, + }, + }) +} + func TestAccAWSInstance_instanceProfileChange(t *testing.T) { var v ec2.Instance rName := acctest.RandString(5) @@ -1281,6 +1330,117 @@ resource "aws_instance" "foo" { } ` +const testAccCheckInstanceConfigNoVolumeTags = ` +resource "aws_instance" "foo" { + ami = "ami-55a7ea65" + + instance_type = "m3.medium" + + root_block_device { + volume_type = "gp2" + volume_size = 11 + } + ebs_block_device { + device_name = "/dev/sdb" + volume_size = 9 + } + ebs_block_device { + device_name = "/dev/sdc" + volume_size = 10 + volume_type = "io1" + iops = 100 + } + + ebs_block_device { + device_name = "/dev/sdd" + volume_size = 12 + encrypted = true + } + + ephemeral_block_device { + device_name = "/dev/sde" + virtual_name = "ephemeral0" + } +} +` + +const testAccCheckInstanceConfigWithVolumeTags = ` +resource "aws_instance" "foo" { + ami = "ami-55a7ea65" + + instance_type = "m3.medium" + + root_block_device { + volume_type = "gp2" + volume_size = 11 + } + ebs_block_device { + device_name = "/dev/sdb" + volume_size = 9 + } + ebs_block_device { + device_name = "/dev/sdc" + volume_size = 10 + volume_type = "io1" + iops = 100 + } + + ebs_block_device { + device_name = "/dev/sdd" + volume_size = 12 + encrypted = true + } + + ephemeral_block_device { + device_name = "/dev/sde" + virtual_name = "ephemeral0" + } + + volume_tags { + Name = "acceptance-test-volume-tag" + } +} +` + +const testAccCheckInstanceConfigWithVolumeTagsUpdate = ` +resource "aws_instance" "foo" { + ami = "ami-55a7ea65" + + instance_type = "m3.medium" + + root_block_device { + volume_type = "gp2" + volume_size = 11 + } + ebs_block_device { + device_name = "/dev/sdb" + volume_size = 9 + } + ebs_block_device { + device_name = "/dev/sdc" + volume_size = 10 + volume_type = "io1" + iops = 100 + } + + ebs_block_device { + device_name = "/dev/sdd" + volume_size = 12 + encrypted = true + } + + ephemeral_block_device { + device_name = "/dev/sde" + virtual_name = "ephemeral0" + } + + volume_tags { + Name = "acceptance-test-volume-tag" + Environment = "dev" + } +} +` + const testAccCheckInstanceConfigTagsUpdate = ` resource "aws_instance" "foo" { ami = "ami-4fccb37f" diff --git a/builtin/providers/aws/tags.go b/builtin/providers/aws/tags.go index 90fda0146..57a8f6ab0 100644 --- a/builtin/providers/aws/tags.go +++ b/builtin/providers/aws/tags.go @@ -69,6 +69,63 @@ func setElbV2Tags(conn *elbv2.ELBV2, d *schema.ResourceData) error { return nil } +func setVolumeTags(conn *ec2.EC2, d *schema.ResourceData) error { + if d.HasChange("volume_tags") { + oraw, nraw := d.GetChange("volume_tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + create, remove := diffTags(tagsFromMap(o), tagsFromMap(n)) + + volumeIds, err := getAwsInstanceVolumeIds(conn, d) + if err != nil { + return err + } + + if len(remove) > 0 { + err := resource.Retry(2*time.Minute, func() *resource.RetryError { + log.Printf("[DEBUG] Removing volume tags: %#v from %s", remove, d.Id()) + _, err := conn.DeleteTags(&ec2.DeleteTagsInput{ + Resources: volumeIds, + Tags: remove, + }) + if err != nil { + ec2err, ok := err.(awserr.Error) + if ok && strings.Contains(ec2err.Code(), ".NotFound") { + return resource.RetryableError(err) // retry + } + return resource.NonRetryableError(err) + } + return nil + }) + if err != nil { + return err + } + } + if len(create) > 0 { + err := resource.Retry(2*time.Minute, func() *resource.RetryError { + log.Printf("[DEBUG] Creating vol tags: %s for %s", create, d.Id()) + _, err := conn.CreateTags(&ec2.CreateTagsInput{ + Resources: volumeIds, + Tags: create, + }) + if err != nil { + ec2err, ok := err.(awserr.Error) + if ok && strings.Contains(ec2err.Code(), ".NotFound") { + return resource.RetryableError(err) // retry + } + return resource.NonRetryableError(err) + } + return nil + }) + if err != nil { + return err + } + } + } + + return nil +} + // setTags is a helper to set the tags for a resource. It expects the // tags field to be named "tags" func setTags(conn *ec2.EC2, d *schema.ResourceData) error { diff --git a/website/source/docs/providers/aws/r/instance.html.markdown b/website/source/docs/providers/aws/r/instance.html.markdown index 4d8725de2..cfcedb07b 100644 --- a/website/source/docs/providers/aws/r/instance.html.markdown +++ b/website/source/docs/providers/aws/r/instance.html.markdown @@ -80,6 +80,7 @@ instances. See [Shutdown Behavior](https://docs.aws.amazon.com/AWSEC2/latest/Use * `ipv6_address_count`- (Optional) A number of IPv6 addresses to associate with the primary network interface. Amazon EC2 chooses the IPv6 addresses from the range of your subnet. * `ipv6_addresses` - (Optional) Specify one or more IPv6 addresses from the range of the subnet to associate with the primary network interface * `tags` - (Optional) A mapping of tags to assign to the resource. +* `volume_tags` - (Optional) A mapping of tags to assign to the devices created by the instance at launch time. * `root_block_device` - (Optional) Customize details about the root block device of the instance. See [Block Devices](#block-devices) below for details. * `ebs_block_device` - (Optional) Additional EBS block devices to attach to the From 0fa5aefb96101aa095ea6afd6442fb6ceedfe19e Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Wed, 26 Apr 2017 10:13:13 +1200 Subject: [PATCH 47/89] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f971216af..415f410f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ IMPROVEMENTS: * provider/aws: Validate WAF metric names [GH-13885] * provider/aws: Allow AWS Subnet to change IPv6 CIDR Block without ForceNew [GH-13909] * provider/aws: Allow filtering of aws_subnet_ids by tags [GH-13937] + * provider/aws: Support aws_instance and volume tagging on creation [GH-13945] * provider/azurerm: VM Scale Sets - import support [GH-13464] * provider/azurerm: Allow Azure China region support [GH-13767] * provider/digitalocean: Export droplet prices [GH-13720] From 8ac89c78609a7e0a8b3ce3fe3a3ed01214b54392 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Tue, 25 Apr 2017 18:15:28 -0400 Subject: [PATCH 48/89] provider/aws: Improve documentation for the cloudformation_stack resource. Fixes: #8483 --- .../docs/providers/aws/r/cloudformation_stack.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/providers/aws/r/cloudformation_stack.html.markdown b/website/source/docs/providers/aws/r/cloudformation_stack.html.markdown index 6948fa97a..8a1a8bbf2 100644 --- a/website/source/docs/providers/aws/r/cloudformation_stack.html.markdown +++ b/website/source/docs/providers/aws/r/cloudformation_stack.html.markdown @@ -73,4 +73,4 @@ The following arguments are supported: The following attributes are exported: * `id` - A unique identifier of the stack. -* `outputs` - A list of output structures. +* `outputs` - A map of outputs from the stack. From 26d6a562f15a18dada492ae13dc7cab58c6212fb Mon Sep 17 00:00:00 2001 From: Bernerd Schaefer Date: Tue, 25 Apr 2017 15:27:01 -0700 Subject: [PATCH 49/89] Use // for inline comments --- website/source/docs/providers/heroku/r/space.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/docs/providers/heroku/r/space.html.markdown b/website/source/docs/providers/heroku/r/space.html.markdown index ae573029b..089fe2159 100644 --- a/website/source/docs/providers/heroku/r/space.html.markdown +++ b/website/source/docs/providers/heroku/r/space.html.markdown @@ -13,14 +13,14 @@ Provides a Heroku Space resource for running apps in isolated, highly available, ## Example Usage ```hcl -# Create a new Heroku space +// Create a new Heroku space resource "heroku_space" "default" { name = "test-space" organization = "my-company" region = "virginia" } -# Create a new Heroku app in test-space +// Create a new Heroku app in test-space resource "heroku_app" "default" { name = "test-app" space = "${heroku_space.default.name}" From e7c904a87d9a4da6f97e30afb64b458c7fddcf3a Mon Sep 17 00:00:00 2001 From: Bernerd Schaefer Date: Tue, 25 Apr 2017 15:32:27 -0700 Subject: [PATCH 50/89] Add note about type conversion --- builtin/providers/heroku/resource_heroku_space.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/builtin/providers/heroku/resource_heroku_space.go b/builtin/providers/heroku/resource_heroku_space.go index 82e21e56d..3e90fffbb 100644 --- a/builtin/providers/heroku/resource_heroku_space.go +++ b/builtin/providers/heroku/resource_heroku_space.go @@ -56,6 +56,8 @@ func resourceHerokuSpaceCreate(d *schema.ResourceData, meta interface{}) error { d.SetId(space.ID) log.Printf("[INFO] Space ID: %s", d.Id()) + // The type conversion here can be dropped when the vendored version of + // heroku-go is updated. setSpaceAttributes(d, (*heroku.Space)(space)) return nil } @@ -68,6 +70,8 @@ func resourceHerokuSpaceRead(d *schema.ResourceData, meta interface{}) error { return err } + // The type conversion here can be dropped when the vendored version of + // heroku-go is updated. setSpaceAttributes(d, (*heroku.Space)(space)) return nil } @@ -87,6 +91,8 @@ func resourceHerokuSpaceUpdate(d *schema.ResourceData, meta interface{}) error { return err } + // The type conversion here can be dropped when the vendored version of + // heroku-go is updated. setSpaceAttributes(d, (*heroku.Space)(space)) return nil } From 381514a70d7eff84e68b7fbca3caa2c9edfee7d1 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Tue, 25 Apr 2017 18:44:57 -0400 Subject: [PATCH 51/89] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 415f410f4..23f0f1b03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ FEATURES: * **New Resource:**  `aws_network_interface_attachment` [GH-13861] * **New Resource:** `github_branch_protection` [GH-10476] * **New Resource:** `google_bigquery_dataset` [GH-13436] +* **New Resource:** `heroku_space` [GH-13921] * **New Resource:** `template_dir` for producing a directory from templates [GH-13652] * **New Interpolation Function:** `coalescelist()` [GH-12537] From b873a4594f593cb1760da02b5753485cde0217c7 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Tue, 25 Apr 2017 18:51:58 -0400 Subject: [PATCH 52/89] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23f0f1b03..520c55fe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ BUG FIXES: * provider/fastly: Add ability to associate a healthcheck to a backend [GH-13539] * provider/google: Stop setting the id when project creation fails [GH-13644] * provider/google: Make ports in resource_compute_forwarding_rule ForceNew [GH-13833] + * provider/google: Validation fixes for forwarding rules [GH-13952] * provider/ignition: Internal cache moved to global, instead per provider instance [GH-13919] * provider/logentries: Refresh from state when resources not found [GH-13810] * provider/newrelic: newrelic_alert_condition - `condition_scope` must be `application` or `instance` [GH-12972] From 76b0eefaccf93293fb6914c223d5526dc9395c51 Mon Sep 17 00:00:00 2001 From: Joshua Spence Date: Wed, 26 Apr 2017 09:48:02 +1000 Subject: [PATCH 53/89] Fix validation of the `name_prefix` parameter of the `aws_alb` resource (#13441) This parameter is being validated using the wrong validation function, which means that we are incorrectly disallowing a `name_prefix` value ending with a dash. --- builtin/providers/aws/resource_aws_alb.go | 2 +- builtin/providers/aws/resource_aws_alb_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/providers/aws/resource_aws_alb.go b/builtin/providers/aws/resource_aws_alb.go index ed756e939..fe94dd0fa 100644 --- a/builtin/providers/aws/resource_aws_alb.go +++ b/builtin/providers/aws/resource_aws_alb.go @@ -48,7 +48,7 @@ func resourceAwsAlb() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, - ValidateFunc: validateElbName, + ValidateFunc: validateElbNamePrefix, }, "internal": { diff --git a/builtin/providers/aws/resource_aws_alb_test.go b/builtin/providers/aws/resource_aws_alb_test.go index fbebb390d..807d1f4e5 100644 --- a/builtin/providers/aws/resource_aws_alb_test.go +++ b/builtin/providers/aws/resource_aws_alb_test.go @@ -113,7 +113,7 @@ func TestAccAWSALB_namePrefix(t *testing.T) { testAccCheckAWSALBExists("aws_alb.alb_test", &conf), resource.TestCheckResourceAttrSet("aws_alb.alb_test", "name"), resource.TestMatchResourceAttr("aws_alb.alb_test", "name", - regexp.MustCompile("^tf-lb")), + regexp.MustCompile("^tf-lb-")), ), }, }, @@ -851,7 +851,7 @@ resource "aws_security_group" "alb_test" { func testAccAWSALBConfig_namePrefix() string { return fmt.Sprintf(` resource "aws_alb" "alb_test" { - name_prefix = "tf-lb" + name_prefix = "tf-lb-" internal = true security_groups = ["${aws_security_group.alb_test.id}"] subnets = ["${aws_subnet.alb_test.*.id}"] From e06a3768427e0fef4ff5d1f1aeccb4825cdce8b0 Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Wed, 26 Apr 2017 11:49:52 +1200 Subject: [PATCH 54/89] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 520c55fe0..e05eda102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ BUG FIXES: * provider/aws: Remove aws_network_acl_rule if not found [GH-13608] * provider/aws: Use mutex & retry for WAF change operations [GH-13656] * provider/aws: Adding support for ipv6 to aws_subnets needs migration [GH-13876] + * provider/aws: Fix validation of the `name_prefix` parameter of the `aws_alb` resource [GH-13441] * provider/azurerm: azurerm_redis_cache resource missing hostname [GH-13650] * provider/azurerm: Locking around Network Security Group / Subnets [GH-13637] * provider/azurerm: Locking route table on subnet create/delete [GH-13791] From dea267218cb2443f71e457bce58e86d517d66a58 Mon Sep 17 00:00:00 2001 From: Kazunori Kojima Date: Wed, 26 Apr 2017 09:07:12 +0900 Subject: [PATCH 55/89] docs: Fix parameter name of aws_listener_rule --- .../docs/providers/aws/r/alb_listener_rule.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/docs/providers/aws/r/alb_listener_rule.html.markdown b/website/source/docs/providers/aws/r/alb_listener_rule.html.markdown index 50de244d0..6bfe263c7 100644 --- a/website/source/docs/providers/aws/r/alb_listener_rule.html.markdown +++ b/website/source/docs/providers/aws/r/alb_listener_rule.html.markdown @@ -63,12 +63,12 @@ The following arguments are supported: * `action` - (Required) An Action block. Action blocks are documented below. * `condition` - (Required) A Condition block. Condition blocks are documented below. -Action Blocks (for `default_action`) support the following: +Action Blocks (for `action`) support the following: * `target_group_arn` - (Required) The ARN of the Target Group to which to route traffic. * `type` - (Required) The type of routing action. The only valid value is `forward`. -Condition Blocks (for `default_condition`) support the following: +Condition Blocks (for `condition`) support the following: * `field` - (Required) The name of the field. Must be one of `path-pattern` for path based routing or `host-header` for host based routing. * `values` - (Required) The path patterns to match. A maximum of 1 can be defined. From 686d10af4ad1138c46b854b676f312f8e358ad20 Mon Sep 17 00:00:00 2001 From: Andrew King Date: Wed, 26 Apr 2017 16:39:14 +1000 Subject: [PATCH 56/89] Update cloudwatch_event_target.html.markdown (#13964) Clean up `Note` section. --- .../docs/providers/aws/r/cloudwatch_event_target.html.markdown | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/source/docs/providers/aws/r/cloudwatch_event_target.html.markdown b/website/source/docs/providers/aws/r/cloudwatch_event_target.html.markdown index 161097863..252beb4c6 100644 --- a/website/source/docs/providers/aws/r/cloudwatch_event_target.html.markdown +++ b/website/source/docs/providers/aws/r/cloudwatch_event_target.html.markdown @@ -47,11 +47,12 @@ resource "aws_kinesis_stream" "test_stream" { ## Argument Reference -> **Note:** `input` and `input_path` are mutually exclusive options. + -> **Note:** In order to be able to have your AWS Lambda function or SNS topic invoked by a CloudWatch Events rule, you must setup the right permissions using [`aws_lambda_permission`](https://www.terraform.io/docs/providers/aws/r/lambda_permission.html) or [`aws_sns_topic.policy`](https://www.terraform.io/docs/providers/aws/r/sns_topic.html#policy). - More info here [here](http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/EventsResourceBasedPermissions.html). + More info [here](http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/EventsResourceBasedPermissions.html). The following arguments are supported: From 6ff114a17820d5e159cfcfbd07a342e6f62f6acf Mon Sep 17 00:00:00 2001 From: Dana Hoffman Date: Wed, 26 Apr 2017 03:35:19 -0700 Subject: [PATCH 57/89] provider/google: fix panic in GKE provisioning with addons (#13954) --- builtin/providers/google/resource_container_cluster.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/providers/google/resource_container_cluster.go b/builtin/providers/google/resource_container_cluster.go index 8b3233111..55805541c 100644 --- a/builtin/providers/google/resource_container_cluster.go +++ b/builtin/providers/google/resource_container_cluster.go @@ -408,14 +408,14 @@ func resourceContainerClusterCreate(d *schema.ResourceData, meta interface{}) er addonsConfig := v.([]interface{})[0].(map[string]interface{}) cluster.AddonsConfig = &container.AddonsConfig{} - if v, ok := addonsConfig["http_load_balancing"]; ok { + if v, ok := addonsConfig["http_load_balancing"]; ok && len(v.([]interface{})) > 0 { addon := v.([]interface{})[0].(map[string]interface{}) cluster.AddonsConfig.HttpLoadBalancing = &container.HttpLoadBalancing{ Disabled: addon["disabled"].(bool), } } - if v, ok := addonsConfig["horizontal_pod_autoscaling"]; ok { + if v, ok := addonsConfig["horizontal_pod_autoscaling"]; ok && len(v.([]interface{})) > 0 { addon := v.([]interface{})[0].(map[string]interface{}) cluster.AddonsConfig.HorizontalPodAutoscaling = &container.HorizontalPodAutoscaling{ Disabled: addon["disabled"].(bool), From 1024976cdf36f70b4887a7b3c7926d7be62cf93b Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 26 Apr 2017 11:39:54 +0100 Subject: [PATCH 58/89] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e05eda102..f40dfdd78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ BUG FIXES: * provider/azurerm: VM's - ignoring the case on the `create_option` field during Diff's [GH-13933] * provider/azurerm: fixing a bug refreshing the `azurerm_redis_cache` [GH-13899] * provider/fastly: Fix issue with using 0 for `default_ttl` [GH-13648] + * provider/google: Fix panic in GKE provisioning with addons [GH-13954] * provider/fastly: Add ability to associate a healthcheck to a backend [GH-13539] * provider/google: Stop setting the id when project creation fails [GH-13644] * provider/google: Make ports in resource_compute_forwarding_rule ForceNew [GH-13833] From 6939345c429d561d8ed62e3481aa09895c159997 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Wed, 26 Apr 2017 08:06:02 -0400 Subject: [PATCH 59/89] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f40dfdd78..67fe4914e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ IMPROVEMENTS: * provider/aws: Allow AWS Subnet to change IPv6 CIDR Block without ForceNew [GH-13909] * provider/aws: Allow filtering of aws_subnet_ids by tags [GH-13937] * provider/aws: Support aws_instance and volume tagging on creation [GH-13945] + * provider/aws: Add network_interface to aws_instance [GH-12933] * provider/azurerm: VM Scale Sets - import support [GH-13464] * provider/azurerm: Allow Azure China region support [GH-13767] * provider/digitalocean: Export droplet prices [GH-13720] From 277bbf65d1141207dbb30485bf124b1dba08f80f Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Wed, 26 Apr 2017 12:35:28 +0000 Subject: [PATCH 60/89] v0.9.4 --- CHANGELOG.md | 382 +++++++++++++++++++++---------------------- terraform/version.go | 2 +- website/config.rb | 2 +- 3 files changed, 193 insertions(+), 193 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67fe4914e..d918b76c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,114 +1,114 @@ -## 0.9.4 (Unreleased) +## 0.9.4 (26th April 2017) BACKWARDS INCOMPATIBILITIES / NOTES: * provider/template: Fix invalid MIME formatting in `template_cloudinit_config`. While the change itself is not breaking the data source it may be referenced e.g. in `aws_launch_configuration` and similar resources which are immutable - and the formatting change will therefore trigger recreation [GH-13752] + and the formatting change will therefore trigger recreation ([#13752](https://github.com/hashicorp/terraform/issues/13752)) FEATURES: -* **New Provider:** `opc` - Oracle Public Cloud [GH-13468] -* **New Provider:** `oneandone` [GH-13633] -* **New Data Source:** `aws_ami_ids` [GH-13844] [GH-13866] -* **New Data Source:** `aws_ebs_snapshot_ids` [GH-13844] [GH-13866] -* **New Data Source:** `aws_kms_alias` [GH-13669] -* **New Data Source:** `aws_kinesis_stream` [GH-13562] -* **New Data Source:** `digitalocean_image` [GH-13787] -* **New Data Source:** `google_compute_network` [GH-12442] -* **New Data Source:** `google_compute_subnetwork` [GH-12442] -* **New Resource:** `local_file` for creating local files (please see the docs for caveats) [GH-12757] -* **New Resource:** `alicloud_ess_scalinggroup` [GH-13731] -* **New Resource:** `alicloud_ess_scalingconfiguration` [GH-13731] -* **New Resource:** `alicloud_ess_scalingrule` [GH-13731] -* **New Resource:** `alicloud_ess_schedule` [GH-13731] -* **New Resource:** `alicloud_snat_entry` [GH-13731] -* **New Resource:** `alicloud_forward_entry` [GH-13731] -* **New Resource:** `aws_cognito_identity_pool` [GH-13783] -* **New Resource:**  `aws_network_interface_attachment` [GH-13861] -* **New Resource:** `github_branch_protection` [GH-10476] -* **New Resource:** `google_bigquery_dataset` [GH-13436] -* **New Resource:** `heroku_space` [GH-13921] -* **New Resource:** `template_dir` for producing a directory from templates [GH-13652] -* **New Interpolation Function:** `coalescelist()` [GH-12537] +* **New Provider:** `opc` - Oracle Public Cloud ([#13468](https://github.com/hashicorp/terraform/issues/13468)) +* **New Provider:** `oneandone` ([#13633](https://github.com/hashicorp/terraform/issues/13633)) +* **New Data Source:** `aws_ami_ids` ([#13844](https://github.com/hashicorp/terraform/issues/13844)] [[#13866](https://github.com/hashicorp/terraform/issues/13866)) +* **New Data Source:** `aws_ebs_snapshot_ids` ([#13844](https://github.com/hashicorp/terraform/issues/13844)] [[#13866](https://github.com/hashicorp/terraform/issues/13866)) +* **New Data Source:** `aws_kms_alias` ([#13669](https://github.com/hashicorp/terraform/issues/13669)) +* **New Data Source:** `aws_kinesis_stream` ([#13562](https://github.com/hashicorp/terraform/issues/13562)) +* **New Data Source:** `digitalocean_image` ([#13787](https://github.com/hashicorp/terraform/issues/13787)) +* **New Data Source:** `google_compute_network` ([#12442](https://github.com/hashicorp/terraform/issues/12442)) +* **New Data Source:** `google_compute_subnetwork` ([#12442](https://github.com/hashicorp/terraform/issues/12442)) +* **New Resource:** `local_file` for creating local files (please see the docs for caveats) ([#12757](https://github.com/hashicorp/terraform/issues/12757)) +* **New Resource:** `alicloud_ess_scalinggroup` ([#13731](https://github.com/hashicorp/terraform/issues/13731)) +* **New Resource:** `alicloud_ess_scalingconfiguration` ([#13731](https://github.com/hashicorp/terraform/issues/13731)) +* **New Resource:** `alicloud_ess_scalingrule` ([#13731](https://github.com/hashicorp/terraform/issues/13731)) +* **New Resource:** `alicloud_ess_schedule` ([#13731](https://github.com/hashicorp/terraform/issues/13731)) +* **New Resource:** `alicloud_snat_entry` ([#13731](https://github.com/hashicorp/terraform/issues/13731)) +* **New Resource:** `alicloud_forward_entry` ([#13731](https://github.com/hashicorp/terraform/issues/13731)) +* **New Resource:** `aws_cognito_identity_pool` ([#13783](https://github.com/hashicorp/terraform/issues/13783)) +* **New Resource:**  `aws_network_interface_attachment` ([#13861](https://github.com/hashicorp/terraform/issues/13861)) +* **New Resource:** `github_branch_protection` ([#10476](https://github.com/hashicorp/terraform/issues/10476)) +* **New Resource:** `google_bigquery_dataset` ([#13436](https://github.com/hashicorp/terraform/issues/13436)) +* **New Resource:** `heroku_space` ([#13921](https://github.com/hashicorp/terraform/issues/13921)) +* **New Resource:** `template_dir` for producing a directory from templates ([#13652](https://github.com/hashicorp/terraform/issues/13652)) +* **New Interpolation Function:** `coalescelist()` ([#12537](https://github.com/hashicorp/terraform/issues/12537)) IMPROVEMENTS: - * core: Add a `-reconfigure` flag to the `init` command, to configure a backend while ignoring any saved configuration [GH-13825] - * helper/schema: Disallow validation+diff suppression on computed fields [GH-13878] - * config: The interpolation function `cidrhost` now accepts a negative host number to count backwards from the end of the range [GH-13765] - * config: New interpolation function `matchkeys` for using values from one list to filter corresponding values from another list using a matching set. [GH-13847] - * state/remote/swift: Support Openstack request logging [GH-13583] - * provider/aws: Add an option to skip getting the supported EC2 platforms [GH-13672] - * provider/aws: Add `name_prefix` support to `aws_cloudwatch_log_group` [GH-13273] - * provider/aws: Add `bucket_prefix` to `aws_s3_bucket` [GH-13274] - * provider/aws: Add replica_source_db to the aws_db_instance datasource [GH-13842] - * provider/aws: Add IPv6 outputs to aws_subnet datasource [GH-13841] - * provider/aws: Exercise SecondaryPrivateIpAddressCount for network interface [GH-10590] - * provider/aws: Expose execution ARN + invoke URL for APIG deployment [GH-13889] - * provider/aws: Expose invoke ARN from Lambda function (for API Gateway) [GH-13890] - * provider/aws: Add tagging support to the 'aws_lambda_function' resource [GH-13873] - * provider/aws: Validate WAF metric names [GH-13885] - * provider/aws: Allow AWS Subnet to change IPv6 CIDR Block without ForceNew [GH-13909] - * provider/aws: Allow filtering of aws_subnet_ids by tags [GH-13937] - * provider/aws: Support aws_instance and volume tagging on creation [GH-13945] - * provider/aws: Add network_interface to aws_instance [GH-12933] - * provider/azurerm: VM Scale Sets - import support [GH-13464] - * provider/azurerm: Allow Azure China region support [GH-13767] - * provider/digitalocean: Export droplet prices [GH-13720] - * provider/fastly: Add support for GCS logging [GH-13553] - * provider/google: `google_compute_address` and `google_compute_global_address` are now importable [GH-13270] - * provider/google: `google_compute_network` is now importable [GH-13834] - * provider/google: add attached_disk field to google_compute_instance [GH-13443] - * provider/heroku: Set App buildpacks from config [GH-13910] - * provider/heroku: Create Heroku app in a private space [GH-13862] - * provider/vault: `vault_generic_secret` resource can now optionally detect drift if it has appropriate access [GH-11776] + * core: Add a `-reconfigure` flag to the `init` command, to configure a backend while ignoring any saved configuration ([#13825](https://github.com/hashicorp/terraform/issues/13825)) + * helper/schema: Disallow validation+diff suppression on computed fields ([#13878](https://github.com/hashicorp/terraform/issues/13878)) + * config: The interpolation function `cidrhost` now accepts a negative host number to count backwards from the end of the range ([#13765](https://github.com/hashicorp/terraform/issues/13765)) + * config: New interpolation function `matchkeys` for using values from one list to filter corresponding values from another list using a matching set. ([#13847](https://github.com/hashicorp/terraform/issues/13847)) + * state/remote/swift: Support Openstack request logging ([#13583](https://github.com/hashicorp/terraform/issues/13583)) + * provider/aws: Add an option to skip getting the supported EC2 platforms ([#13672](https://github.com/hashicorp/terraform/issues/13672)) + * provider/aws: Add `name_prefix` support to `aws_cloudwatch_log_group` ([#13273](https://github.com/hashicorp/terraform/issues/13273)) + * provider/aws: Add `bucket_prefix` to `aws_s3_bucket` ([#13274](https://github.com/hashicorp/terraform/issues/13274)) + * provider/aws: Add replica_source_db to the aws_db_instance datasource ([#13842](https://github.com/hashicorp/terraform/issues/13842)) + * provider/aws: Add IPv6 outputs to aws_subnet datasource ([#13841](https://github.com/hashicorp/terraform/issues/13841)) + * provider/aws: Exercise SecondaryPrivateIpAddressCount for network interface ([#10590](https://github.com/hashicorp/terraform/issues/10590)) + * provider/aws: Expose execution ARN + invoke URL for APIG deployment ([#13889](https://github.com/hashicorp/terraform/issues/13889)) + * provider/aws: Expose invoke ARN from Lambda function (for API Gateway) ([#13890](https://github.com/hashicorp/terraform/issues/13890)) + * provider/aws: Add tagging support to the 'aws_lambda_function' resource ([#13873](https://github.com/hashicorp/terraform/issues/13873)) + * provider/aws: Validate WAF metric names ([#13885](https://github.com/hashicorp/terraform/issues/13885)) + * provider/aws: Allow AWS Subnet to change IPv6 CIDR Block without ForceNew ([#13909](https://github.com/hashicorp/terraform/issues/13909)) + * provider/aws: Allow filtering of aws_subnet_ids by tags ([#13937](https://github.com/hashicorp/terraform/issues/13937)) + * provider/aws: Support aws_instance and volume tagging on creation ([#13945](https://github.com/hashicorp/terraform/issues/13945)) + * provider/aws: Add network_interface to aws_instance ([#12933](https://github.com/hashicorp/terraform/issues/12933)) + * provider/azurerm: VM Scale Sets - import support ([#13464](https://github.com/hashicorp/terraform/issues/13464)) + * provider/azurerm: Allow Azure China region support ([#13767](https://github.com/hashicorp/terraform/issues/13767)) + * provider/digitalocean: Export droplet prices ([#13720](https://github.com/hashicorp/terraform/issues/13720)) + * provider/fastly: Add support for GCS logging ([#13553](https://github.com/hashicorp/terraform/issues/13553)) + * provider/google: `google_compute_address` and `google_compute_global_address` are now importable ([#13270](https://github.com/hashicorp/terraform/issues/13270)) + * provider/google: `google_compute_network` is now importable ([#13834](https://github.com/hashicorp/terraform/issues/13834)) + * provider/google: add attached_disk field to google_compute_instance ([#13443](https://github.com/hashicorp/terraform/issues/13443)) + * provider/heroku: Set App buildpacks from config ([#13910](https://github.com/hashicorp/terraform/issues/13910)) + * provider/heroku: Create Heroku app in a private space ([#13862](https://github.com/hashicorp/terraform/issues/13862)) + * provider/vault: `vault_generic_secret` resource can now optionally detect drift if it has appropriate access ([#11776](https://github.com/hashicorp/terraform/issues/11776)) BUG FIXES: - * core: Prevent resource.Retry from adding untracked resources after the timeout: [GH-13778] - * core: Allow a schema.TypeList to be ForceNew and computed [GH-13863] - * core: Fix crash when refresh or apply build an invalid graph [GH-13665] - * core: Add the close provider/provisioner transformers back [GH-13102] - * core: Fix a crash condition by improving the flatmap.Expand() logic [GH-13541] - * provider/alicloud: Fix create PrePaid instance [GH-13662] - * provider/alicloud: Fix allocate public ip error [GH-13268] - * provider/alicloud: alicloud_security_group_rule: check ptr before use it [GH-13731) - * provider/alicloud: alicloud_instance: fix ecs internet_max_bandwidth_out cannot set zero bug [GH-13731] - * provider/aws: Allow force-destroying `aws_route53_zone` which has trailing dot [GH-12421] - * provider/aws: Allow GovCloud KMS ARNs to pass validation in `kms_key_id` attributes [GH-13699] - * provider/aws: Changing aws_opsworks_instance should ForceNew [GH-13839] - * provider/aws: Fix DB Parameter Group Name [GH-13279] - * provider/aws: Fix issue importing some Security Groups and Rules based on rule structure [GH-13630] - * provider/aws: Fix issue for cross account IAM role with `aws_lambda_permission` [GH-13865] - * provider/aws: Fix WAF IPSet descriptors removal on update [GH-13766] - * provider/aws: Increase default number of retries from 11 to 25 [GH-13673] - * provider/aws: Remove aws_vpc_dhcp_options if not found [GH-13610] - * provider/aws: Remove aws_network_acl_rule if not found [GH-13608] - * provider/aws: Use mutex & retry for WAF change operations [GH-13656] - * provider/aws: Adding support for ipv6 to aws_subnets needs migration [GH-13876] - * provider/aws: Fix validation of the `name_prefix` parameter of the `aws_alb` resource [GH-13441] - * provider/azurerm: azurerm_redis_cache resource missing hostname [GH-13650] - * provider/azurerm: Locking around Network Security Group / Subnets [GH-13637] - * provider/azurerm: Locking route table on subnet create/delete [GH-13791] - * provider/azurerm: VM's - fixes a bug where ssh_keys could contain a null entry [GH-13755] - * provider/azurerm: VM's - ignoring the case on the `create_option` field during Diff's [GH-13933] - * provider/azurerm: fixing a bug refreshing the `azurerm_redis_cache` [GH-13899] - * provider/fastly: Fix issue with using 0 for `default_ttl` [GH-13648] - * provider/google: Fix panic in GKE provisioning with addons [GH-13954] - * provider/fastly: Add ability to associate a healthcheck to a backend [GH-13539] - * provider/google: Stop setting the id when project creation fails [GH-13644] - * provider/google: Make ports in resource_compute_forwarding_rule ForceNew [GH-13833] - * provider/google: Validation fixes for forwarding rules [GH-13952] - * provider/ignition: Internal cache moved to global, instead per provider instance [GH-13919] - * provider/logentries: Refresh from state when resources not found [GH-13810] - * provider/newrelic: newrelic_alert_condition - `condition_scope` must be `application` or `instance` [GH-12972] - * provider/opc: fixed issue with unqualifying nats [GH-13826] - * provider/opc: Fix instance label if unset [GH-13846] - * provider/openstack: Fix updating Ports [GH-13604] - * provider/rabbitmq: Allow users without tags [GH-13798] + * core: Prevent resource.Retry from adding untracked resources after the timeout: ([#13778](https://github.com/hashicorp/terraform/issues/13778)) + * core: Allow a schema.TypeList to be ForceNew and computed ([#13863](https://github.com/hashicorp/terraform/issues/13863)) + * core: Fix crash when refresh or apply build an invalid graph ([#13665](https://github.com/hashicorp/terraform/issues/13665)) + * core: Add the close provider/provisioner transformers back ([#13102](https://github.com/hashicorp/terraform/issues/13102)) + * core: Fix a crash condition by improving the flatmap.Expand() logic ([#13541](https://github.com/hashicorp/terraform/issues/13541)) + * provider/alicloud: Fix create PrePaid instance ([#13662](https://github.com/hashicorp/terraform/issues/13662)) + * provider/alicloud: Fix allocate public ip error ([#13268](https://github.com/hashicorp/terraform/issues/13268)) + * provider/alicloud: alicloud_security_group_rule: check ptr before use it [[#13731](https://github.com/hashicorp/terraform/issues/13731)) + * provider/alicloud: alicloud_instance: fix ecs internet_max_bandwidth_out cannot set zero bug ([#13731](https://github.com/hashicorp/terraform/issues/13731)) + * provider/aws: Allow force-destroying `aws_route53_zone` which has trailing dot ([#12421](https://github.com/hashicorp/terraform/issues/12421)) + * provider/aws: Allow GovCloud KMS ARNs to pass validation in `kms_key_id` attributes ([#13699](https://github.com/hashicorp/terraform/issues/13699)) + * provider/aws: Changing aws_opsworks_instance should ForceNew ([#13839](https://github.com/hashicorp/terraform/issues/13839)) + * provider/aws: Fix DB Parameter Group Name ([#13279](https://github.com/hashicorp/terraform/issues/13279)) + * provider/aws: Fix issue importing some Security Groups and Rules based on rule structure ([#13630](https://github.com/hashicorp/terraform/issues/13630)) + * provider/aws: Fix issue for cross account IAM role with `aws_lambda_permission` ([#13865](https://github.com/hashicorp/terraform/issues/13865)) + * provider/aws: Fix WAF IPSet descriptors removal on update ([#13766](https://github.com/hashicorp/terraform/issues/13766)) + * provider/aws: Increase default number of retries from 11 to 25 ([#13673](https://github.com/hashicorp/terraform/issues/13673)) + * provider/aws: Remove aws_vpc_dhcp_options if not found ([#13610](https://github.com/hashicorp/terraform/issues/13610)) + * provider/aws: Remove aws_network_acl_rule if not found ([#13608](https://github.com/hashicorp/terraform/issues/13608)) + * provider/aws: Use mutex & retry for WAF change operations ([#13656](https://github.com/hashicorp/terraform/issues/13656)) + * provider/aws: Adding support for ipv6 to aws_subnets needs migration ([#13876](https://github.com/hashicorp/terraform/issues/13876)) + * provider/aws: Fix validation of the `name_prefix` parameter of the `aws_alb` resource ([#13441](https://github.com/hashicorp/terraform/issues/13441)) + * provider/azurerm: azurerm_redis_cache resource missing hostname ([#13650](https://github.com/hashicorp/terraform/issues/13650)) + * provider/azurerm: Locking around Network Security Group / Subnets ([#13637](https://github.com/hashicorp/terraform/issues/13637)) + * provider/azurerm: Locking route table on subnet create/delete ([#13791](https://github.com/hashicorp/terraform/issues/13791)) + * provider/azurerm: VM's - fixes a bug where ssh_keys could contain a null entry ([#13755](https://github.com/hashicorp/terraform/issues/13755)) + * provider/azurerm: VM's - ignoring the case on the `create_option` field during Diff's ([#13933](https://github.com/hashicorp/terraform/issues/13933)) + * provider/azurerm: fixing a bug refreshing the `azurerm_redis_cache` [[#13899](https://github.com/hashicorp/terraform/issues/13899)] + * provider/fastly: Fix issue with using 0 for `default_ttl` ([#13648](https://github.com/hashicorp/terraform/issues/13648)) + * provider/google: Fix panic in GKE provisioning with addons ([#13954](https://github.com/hashicorp/terraform/issues/13954)) + * provider/fastly: Add ability to associate a healthcheck to a backend ([#13539](https://github.com/hashicorp/terraform/issues/13539)) + * provider/google: Stop setting the id when project creation fails ([#13644](https://github.com/hashicorp/terraform/issues/13644)) + * provider/google: Make ports in resource_compute_forwarding_rule ForceNew ([#13833](https://github.com/hashicorp/terraform/issues/13833)) + * provider/google: Validation fixes for forwarding rules ([#13952](https://github.com/hashicorp/terraform/issues/13952)) + * provider/ignition: Internal cache moved to global, instead per provider instance ([#13919](https://github.com/hashicorp/terraform/issues/13919)) + * provider/logentries: Refresh from state when resources not found ([#13810](https://github.com/hashicorp/terraform/issues/13810)) + * provider/newrelic: newrelic_alert_condition - `condition_scope` must be `application` or `instance` ([#12972](https://github.com/hashicorp/terraform/issues/12972)) + * provider/opc: fixed issue with unqualifying nats ([#13826](https://github.com/hashicorp/terraform/issues/13826)) + * provider/opc: Fix instance label if unset ([#13846](https://github.com/hashicorp/terraform/issues/13846)) + * provider/openstack: Fix updating Ports ([#13604](https://github.com/hashicorp/terraform/issues/13604)) + * provider/rabbitmq: Allow users without tags ([#13798](https://github.com/hashicorp/terraform/issues/13798)) ## 0.9.3 (April 12, 2017) @@ -116,111 +116,111 @@ BACKWARDS INCOMPATIBILITIES / NOTES: * provider/aws: Fix a critical bug in `aws_emr_cluster` in order to preserve the ordering of any arguments in `bootstrap_action`. Terraform will now enforce the ordering from the configuration. As a result, `aws_emr_cluster` resources may need to be - recreated, as there is no API to update them in-place [GH-13580] + recreated, as there is no API to update them in-place ([#13580](https://github.com/hashicorp/terraform/issues/13580)) FEATURES: - * **New Resource:** `aws_api_gateway_method_settings` [GH-13542] - * **New Resource:** `aws_api_gateway_stage` [GH-13540] - * **New Resource:** `aws_iam_openid_connect_provider` [GH-13456] - * **New Resource:** `aws_lightsail_static_ip` [GH-13175] - * **New Resource:** `aws_lightsail_static_ip_attachment` [GH-13207] - * **New Resource:** `aws_ses_domain_identity` [GH-13098] - * **New Resource:** `azurerm_managed_disk` [GH-12455] - * **New Resource:** `kubernetes_persistent_volume` [GH-13277] - * **New Resource:** `kubernetes_persistent_volume_claim` [GH-13527] - * **New Resource:** `kubernetes_secret` [GH-12960] - * **New Data Source:** `aws_iam_role` [GH-13213] + * **New Resource:** `aws_api_gateway_method_settings` ([#13542](https://github.com/hashicorp/terraform/issues/13542)) + * **New Resource:** `aws_api_gateway_stage` ([#13540](https://github.com/hashicorp/terraform/issues/13540)) + * **New Resource:** `aws_iam_openid_connect_provider` ([#13456](https://github.com/hashicorp/terraform/issues/13456)) + * **New Resource:** `aws_lightsail_static_ip` ([#13175](https://github.com/hashicorp/terraform/issues/13175)) + * **New Resource:** `aws_lightsail_static_ip_attachment` ([#13207](https://github.com/hashicorp/terraform/issues/13207)) + * **New Resource:** `aws_ses_domain_identity` ([#13098](https://github.com/hashicorp/terraform/issues/13098)) + * **New Resource:** `azurerm_managed_disk` ([#12455](https://github.com/hashicorp/terraform/issues/12455)) + * **New Resource:** `kubernetes_persistent_volume` ([#13277](https://github.com/hashicorp/terraform/issues/13277)) + * **New Resource:** `kubernetes_persistent_volume_claim` ([#13527](https://github.com/hashicorp/terraform/issues/13527)) + * **New Resource:** `kubernetes_secret` ([#12960](https://github.com/hashicorp/terraform/issues/12960)) + * **New Data Source:** `aws_iam_role` ([#13213](https://github.com/hashicorp/terraform/issues/13213)) IMPROVEMENTS: - * core: add `-lock-timeout` option, which will block and retry locks for the given duration [GH-13262] - * core: new `chomp` interpolation function which returns the given string with any trailing newline characters removed [GH-13419] - * backend/remote-state: Add support for assume role extensions to s3 backend [GH-13236] - * backend/remote-state: Filter extra entries from s3 environment listings [GH-13596] - * config: New interpolation functions `basename` and `dirname`, for file path manipulation [GH-13080] - * helper/resource: Allow unknown "pending" states [GH-13099] - * command/hook_ui: Increase max length of state IDs from 20 to 80 [GH-13317] - * provider/aws: Add support to set iam_role_arn on cloudformation Stack [GH-12547] - * provider/aws: Support priority and listener_arn update of alb_listener_rule [GH-13125] - * provider/aws: Deprecate roles in favour of role in iam_instance_profile [GH-13130] - * provider/aws: Make alb_target_group_attachment port optional [GH-13139] - * provider/aws: `aws_api_gateway_domain_name` `certificate_private_key` field marked as sensitive [GH-13147] - * provider/aws: `aws_directory_service_directory` `password` field marked as sensitive [GH-13147] - * provider/aws: `aws_kinesis_firehose_delivery_stream` `password` field marked as sensitive [GH-13147] - * provider/aws: `aws_opsworks_application` `app_source.0.password` & `ssl_configuration.0.private_key` fields marked as sensitive [GH-13147] - * provider/aws: `aws_opsworks_stack` `custom_cookbooks_source.0.password` field marked as sensitive [GH-13147] - * provider/aws: Support the ability to enable / disable ipv6 support in VPC [GH-12527] - * provider/aws: Added API Gateway integration update [GH-13249] - * provider/aws: Add `identifier` | `name_prefix` to RDS resources [GH-13232] - * provider/aws: Validate `aws_ecs_task_definition.container_definitions` [GH-12161] - * provider/aws: Update caller_identity data source [GH-13092] - * provider/aws: `aws_subnet_ids` data source for getting a list of subnet ids matching certain criteria [GH-13188] - * provider/aws: Support ip_address_type for aws_alb [GH-13227] - * provider/aws: Migrate `aws_dms_*` resources away from AWS waiters [GH-13291] - * provider/aws: Add support for treat_missing_data to cloudwatch_metric_alarm [GH-13358] - * provider/aws: Add support for evaluate_low_sample_count_percentiles to cloudwatch_metric_alarm [GH-13371] - * provider/aws: Add `name_prefix` to `aws_alb_target_group` [GH-13442] - * provider/aws: Add support for EMR clusters to aws_appautoscaling_target [GH-13368] - * provider/aws: Add import capabilities to codecommit_repository [GH-13577] - * provider/bitbucket: Improved error handling [GH-13390] - * provider/cloudstack: Do not force a new resource when updating `cloudstack_loadbalancer_rule` members [GH-11786] - * provider/fastly: Add support for Sumologic logging [GH-12541] - * provider/github: Handle the case when issue labels already exist [GH-13182] - * provider/google: Mark `google_container_cluster`'s `client_key` & `password` inside `master_auth` as sensitive [GH-13148] - * provider/google: Add node_pool field in resource_container_cluster [GH-13402] - * provider/kubernetes: Allow defining custom config context [GH-12958] - * provider/openstack: Add support for 'value_specs' options to `openstack_compute_servergroup_v2` [GH-13380] - * provider/statuscake: Add support for StatusCake TriggerRate field [GH-13340] - * provider/triton: Move to joyent/triton-go [GH-13225] - * provisioner/chef: Make sure we add new Chef-Vault clients as clients [GH-13525] + * core: add `-lock-timeout` option, which will block and retry locks for the given duration ([#13262](https://github.com/hashicorp/terraform/issues/13262)) + * core: new `chomp` interpolation function which returns the given string with any trailing newline characters removed ([#13419](https://github.com/hashicorp/terraform/issues/13419)) + * backend/remote-state: Add support for assume role extensions to s3 backend ([#13236](https://github.com/hashicorp/terraform/issues/13236)) + * backend/remote-state: Filter extra entries from s3 environment listings ([#13596](https://github.com/hashicorp/terraform/issues/13596)) + * config: New interpolation functions `basename` and `dirname`, for file path manipulation ([#13080](https://github.com/hashicorp/terraform/issues/13080)) + * helper/resource: Allow unknown "pending" states ([#13099](https://github.com/hashicorp/terraform/issues/13099)) + * command/hook_ui: Increase max length of state IDs from 20 to 80 ([#13317](https://github.com/hashicorp/terraform/issues/13317)) + * provider/aws: Add support to set iam_role_arn on cloudformation Stack ([#12547](https://github.com/hashicorp/terraform/issues/12547)) + * provider/aws: Support priority and listener_arn update of alb_listener_rule ([#13125](https://github.com/hashicorp/terraform/issues/13125)) + * provider/aws: Deprecate roles in favour of role in iam_instance_profile ([#13130](https://github.com/hashicorp/terraform/issues/13130)) + * provider/aws: Make alb_target_group_attachment port optional ([#13139](https://github.com/hashicorp/terraform/issues/13139)) + * provider/aws: `aws_api_gateway_domain_name` `certificate_private_key` field marked as sensitive ([#13147](https://github.com/hashicorp/terraform/issues/13147)) + * provider/aws: `aws_directory_service_directory` `password` field marked as sensitive ([#13147](https://github.com/hashicorp/terraform/issues/13147)) + * provider/aws: `aws_kinesis_firehose_delivery_stream` `password` field marked as sensitive ([#13147](https://github.com/hashicorp/terraform/issues/13147)) + * provider/aws: `aws_opsworks_application` `app_source.0.password` & `ssl_configuration.0.private_key` fields marked as sensitive ([#13147](https://github.com/hashicorp/terraform/issues/13147)) + * provider/aws: `aws_opsworks_stack` `custom_cookbooks_source.0.password` field marked as sensitive ([#13147](https://github.com/hashicorp/terraform/issues/13147)) + * provider/aws: Support the ability to enable / disable ipv6 support in VPC ([#12527](https://github.com/hashicorp/terraform/issues/12527)) + * provider/aws: Added API Gateway integration update ([#13249](https://github.com/hashicorp/terraform/issues/13249)) + * provider/aws: Add `identifier` | `name_prefix` to RDS resources ([#13232](https://github.com/hashicorp/terraform/issues/13232)) + * provider/aws: Validate `aws_ecs_task_definition.container_definitions` ([#12161](https://github.com/hashicorp/terraform/issues/12161)) + * provider/aws: Update caller_identity data source ([#13092](https://github.com/hashicorp/terraform/issues/13092)) + * provider/aws: `aws_subnet_ids` data source for getting a list of subnet ids matching certain criteria ([#13188](https://github.com/hashicorp/terraform/issues/13188)) + * provider/aws: Support ip_address_type for aws_alb ([#13227](https://github.com/hashicorp/terraform/issues/13227)) + * provider/aws: Migrate `aws_dms_*` resources away from AWS waiters ([#13291](https://github.com/hashicorp/terraform/issues/13291)) + * provider/aws: Add support for treat_missing_data to cloudwatch_metric_alarm ([#13358](https://github.com/hashicorp/terraform/issues/13358)) + * provider/aws: Add support for evaluate_low_sample_count_percentiles to cloudwatch_metric_alarm ([#13371](https://github.com/hashicorp/terraform/issues/13371)) + * provider/aws: Add `name_prefix` to `aws_alb_target_group` ([#13442](https://github.com/hashicorp/terraform/issues/13442)) + * provider/aws: Add support for EMR clusters to aws_appautoscaling_target ([#13368](https://github.com/hashicorp/terraform/issues/13368)) + * provider/aws: Add import capabilities to codecommit_repository ([#13577](https://github.com/hashicorp/terraform/issues/13577)) + * provider/bitbucket: Improved error handling ([#13390](https://github.com/hashicorp/terraform/issues/13390)) + * provider/cloudstack: Do not force a new resource when updating `cloudstack_loadbalancer_rule` members ([#11786](https://github.com/hashicorp/terraform/issues/11786)) + * provider/fastly: Add support for Sumologic logging ([#12541](https://github.com/hashicorp/terraform/issues/12541)) + * provider/github: Handle the case when issue labels already exist ([#13182](https://github.com/hashicorp/terraform/issues/13182)) + * provider/google: Mark `google_container_cluster`'s `client_key` & `password` inside `master_auth` as sensitive ([#13148](https://github.com/hashicorp/terraform/issues/13148)) + * provider/google: Add node_pool field in resource_container_cluster ([#13402](https://github.com/hashicorp/terraform/issues/13402)) + * provider/kubernetes: Allow defining custom config context ([#12958](https://github.com/hashicorp/terraform/issues/12958)) + * provider/openstack: Add support for 'value_specs' options to `openstack_compute_servergroup_v2` ([#13380](https://github.com/hashicorp/terraform/issues/13380)) + * provider/statuscake: Add support for StatusCake TriggerRate field ([#13340](https://github.com/hashicorp/terraform/issues/13340)) + * provider/triton: Move to joyent/triton-go ([#13225](https://github.com/hashicorp/terraform/issues/13225)) + * provisioner/chef: Make sure we add new Chef-Vault clients as clients ([#13525](https://github.com/hashicorp/terraform/issues/13525)) BUG FIXES: - * core: Escaped interpolation-like sequences (like `$${foo}`) now permitted in variable defaults [GH-13137] - * core: Fix strange issues with computed values in provider configuration that were worked around with `-input=false` [GH-11264], [GH-13264] - * core: Fix crash when providing nested maps as variable values in a `module` block [GH-13343] - * core: `connection` block attributes are now subject to basic validation of attribute names during validate walk [GH-13400] - * provider/aws: Add Support for maintenance_window and back_window to rds_cluster_instance [GH-13134] - * provider/aws: Increase timeout for AMI registration [GH-13159] - * provider/aws: Increase timeouts for ELB [GH-13161] - * provider/aws: `volume_type` of `aws_elasticsearch_domain.0.ebs_options` marked as `Computed` which prevents spurious diffs [GH-13160] - * provider/aws: Don't set DBName on `aws_db_instance` from snapshot [GH-13140] - * provider/aws: Add DiffSuppression to aws_ecs_service placement_strategies [GH-13220] - * provider/aws: Refresh aws_alb_target_group stickiness on manual updates [GH-13199] - * provider/aws: Preserve default retain_on_delete in cloudfront import [GH-13209] - * provider/aws: Refresh aws_alb_target_group tags [GH-13200] - * provider/aws: Set aws_vpn_connection to recreate when in deleted state [GH-13204] - * provider/aws: Wait for aws_opsworks_instance to be running when it's specified [GH-13218] - * provider/aws: Handle `aws_lambda_function` missing s3 key error [GH-10960] - * provider/aws: Set stickiness to computed in alb_target_group [GH-13278] - * provider/aws: Increase timeout for deploying `cloudfront_distribution` from 40 to 70 mins [GH-13319] - * provider/aws: Increase AMI retry timeouts [GH-13324] - * provider/aws: Increase subnet deletion timeout [GH-13356] - * provider/aws: Increase launch_configuration creation timeout [GH-13357] - * provider/aws: Increase Beanstalk env 'ready' timeout [GH-13359] - * provider/aws: Raise timeout for deleting APIG REST API [GH-13414] - * provider/aws: Raise timeout for attaching/detaching VPN Gateway [GH-13457] - * provider/aws: Recreate opsworks_stack on change of service_role_arn [GH-13325] - * provider/aws: Fix KMS Key reading with Exists method [GH-13348] - * provider/aws: Fix DynamoDB issues about GSIs indexes [GH-13256] - * provider/aws: Fix `aws_s3_bucket` drift detection of logging options [GH-13281] - * provider/aws: Update ElasticTranscoderPreset to have default for MaxFrameRate [GH-13422] - * provider/aws: Fix aws_ami_launch_permission refresh when AMI disappears [GH-13469] - * provider/aws: Add support for updating SSM documents [GH-13491] - * provider/aws: Fix panic on nil route configs [GH-13548] - * provider/azurerm: Network Security Group - ignoring protocol casing at Import time [GH-13153] - * provider/azurerm: Fix crash when importing Local Network Gateways [GH-13261] - * provider/azurerm: Defaulting the value of `duplicate_detection_history_time_window` for `azurerm_servicebus_topic` [GH-13223] - * provider/azurerm: Event Hubs making the Location field idempotent [GH-13570] - * provider/bitbucket: Fixed issue where provider would fail with an "EOF" error on some operations [GH-13390] - * provider/dnsimple: Handle 404 on DNSimple records [GH-13131] - * provider/kubernetes: Use PATCH to update namespace [GH-13114] - * provider/ns1: No splitting answer on SPF records. [GH-13260] - * provider/openstack: Refresh volume_attachment from state if NotFound [GH-13342] - * provider/openstack: Add SOFT_DELETED to delete status [GH-13444] - * provider/profitbricks: Changed output type of ips variable of ip_block ProfitBricks resource [GH-13290] - * provider/template: Fix panic in cloudinit config [GH-13581] + * core: Escaped interpolation-like sequences (like `$${foo}`) now permitted in variable defaults ([#13137](https://github.com/hashicorp/terraform/issues/13137)) + * core: Fix strange issues with computed values in provider configuration that were worked around with `-input=false` ([#11264](https://github.com/hashicorp/terraform/issues/11264)], [[#13264](https://github.com/hashicorp/terraform/issues/13264)) + * core: Fix crash when providing nested maps as variable values in a `module` block ([#13343](https://github.com/hashicorp/terraform/issues/13343)) + * core: `connection` block attributes are now subject to basic validation of attribute names during validate walk ([#13400](https://github.com/hashicorp/terraform/issues/13400)) + * provider/aws: Add Support for maintenance_window and back_window to rds_cluster_instance ([#13134](https://github.com/hashicorp/terraform/issues/13134)) + * provider/aws: Increase timeout for AMI registration ([#13159](https://github.com/hashicorp/terraform/issues/13159)) + * provider/aws: Increase timeouts for ELB ([#13161](https://github.com/hashicorp/terraform/issues/13161)) + * provider/aws: `volume_type` of `aws_elasticsearch_domain.0.ebs_options` marked as `Computed` which prevents spurious diffs ([#13160](https://github.com/hashicorp/terraform/issues/13160)) + * provider/aws: Don't set DBName on `aws_db_instance` from snapshot ([#13140](https://github.com/hashicorp/terraform/issues/13140)) + * provider/aws: Add DiffSuppression to aws_ecs_service placement_strategies ([#13220](https://github.com/hashicorp/terraform/issues/13220)) + * provider/aws: Refresh aws_alb_target_group stickiness on manual updates ([#13199](https://github.com/hashicorp/terraform/issues/13199)) + * provider/aws: Preserve default retain_on_delete in cloudfront import ([#13209](https://github.com/hashicorp/terraform/issues/13209)) + * provider/aws: Refresh aws_alb_target_group tags ([#13200](https://github.com/hashicorp/terraform/issues/13200)) + * provider/aws: Set aws_vpn_connection to recreate when in deleted state ([#13204](https://github.com/hashicorp/terraform/issues/13204)) + * provider/aws: Wait for aws_opsworks_instance to be running when it's specified ([#13218](https://github.com/hashicorp/terraform/issues/13218)) + * provider/aws: Handle `aws_lambda_function` missing s3 key error ([#10960](https://github.com/hashicorp/terraform/issues/10960)) + * provider/aws: Set stickiness to computed in alb_target_group ([#13278](https://github.com/hashicorp/terraform/issues/13278)) + * provider/aws: Increase timeout for deploying `cloudfront_distribution` from 40 to 70 mins ([#13319](https://github.com/hashicorp/terraform/issues/13319)) + * provider/aws: Increase AMI retry timeouts ([#13324](https://github.com/hashicorp/terraform/issues/13324)) + * provider/aws: Increase subnet deletion timeout ([#13356](https://github.com/hashicorp/terraform/issues/13356)) + * provider/aws: Increase launch_configuration creation timeout ([#13357](https://github.com/hashicorp/terraform/issues/13357)) + * provider/aws: Increase Beanstalk env 'ready' timeout ([#13359](https://github.com/hashicorp/terraform/issues/13359)) + * provider/aws: Raise timeout for deleting APIG REST API ([#13414](https://github.com/hashicorp/terraform/issues/13414)) + * provider/aws: Raise timeout for attaching/detaching VPN Gateway ([#13457](https://github.com/hashicorp/terraform/issues/13457)) + * provider/aws: Recreate opsworks_stack on change of service_role_arn ([#13325](https://github.com/hashicorp/terraform/issues/13325)) + * provider/aws: Fix KMS Key reading with Exists method ([#13348](https://github.com/hashicorp/terraform/issues/13348)) + * provider/aws: Fix DynamoDB issues about GSIs indexes ([#13256](https://github.com/hashicorp/terraform/issues/13256)) + * provider/aws: Fix `aws_s3_bucket` drift detection of logging options ([#13281](https://github.com/hashicorp/terraform/issues/13281)) + * provider/aws: Update ElasticTranscoderPreset to have default for MaxFrameRate ([#13422](https://github.com/hashicorp/terraform/issues/13422)) + * provider/aws: Fix aws_ami_launch_permission refresh when AMI disappears ([#13469](https://github.com/hashicorp/terraform/issues/13469)) + * provider/aws: Add support for updating SSM documents ([#13491](https://github.com/hashicorp/terraform/issues/13491)) + * provider/aws: Fix panic on nil route configs ([#13548](https://github.com/hashicorp/terraform/issues/13548)) + * provider/azurerm: Network Security Group - ignoring protocol casing at Import time ([#13153](https://github.com/hashicorp/terraform/issues/13153)) + * provider/azurerm: Fix crash when importing Local Network Gateways ([#13261](https://github.com/hashicorp/terraform/issues/13261)) + * provider/azurerm: Defaulting the value of `duplicate_detection_history_time_window` for `azurerm_servicebus_topic` ([#13223](https://github.com/hashicorp/terraform/issues/13223)) + * provider/azurerm: Event Hubs making the Location field idempotent ([#13570](https://github.com/hashicorp/terraform/issues/13570)) + * provider/bitbucket: Fixed issue where provider would fail with an "EOF" error on some operations ([#13390](https://github.com/hashicorp/terraform/issues/13390)) + * provider/dnsimple: Handle 404 on DNSimple records ([#13131](https://github.com/hashicorp/terraform/issues/13131)) + * provider/kubernetes: Use PATCH to update namespace ([#13114](https://github.com/hashicorp/terraform/issues/13114)) + * provider/ns1: No splitting answer on SPF records. ([#13260](https://github.com/hashicorp/terraform/issues/13260)) + * provider/openstack: Refresh volume_attachment from state if NotFound ([#13342](https://github.com/hashicorp/terraform/issues/13342)) + * provider/openstack: Add SOFT_DELETED to delete status ([#13444](https://github.com/hashicorp/terraform/issues/13444)) + * provider/profitbricks: Changed output type of ips variable of ip_block ProfitBricks resource ([#13290](https://github.com/hashicorp/terraform/issues/13290)) + * provider/template: Fix panic in cloudinit config ([#13581](https://github.com/hashicorp/terraform/issues/13581)) ## 0.9.2 (March 28, 2017) diff --git a/terraform/version.go b/terraform/version.go index 5dbc57fce..e184dc5a6 100644 --- a/terraform/version.go +++ b/terraform/version.go @@ -12,7 +12,7 @@ const Version = "0.9.4" // A pre-release marker for the version. If this is "" (empty string) // then it means that it is a final release. Otherwise, this is a pre-release // such as "dev" (in development), "beta", "rc1", etc. -const VersionPrerelease = "dev" +const VersionPrerelease = "" // SemVersion is an instance of version.Version. This has the secondary // benefit of verifying during tests and init time that our version is a diff --git a/website/config.rb b/website/config.rb index ed989b37d..52ebbd14b 100644 --- a/website/config.rb +++ b/website/config.rb @@ -2,7 +2,7 @@ set :base_url, "https://www.terraform.io/" activate :hashicorp do |h| h.name = "terraform" - h.version = "0.9.3" + h.version = "0.9.4" h.github_slug = "hashicorp/terraform" end From 74eec4fab543b5b8c0c192f7a713a2b24a188e40 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Wed, 26 Apr 2017 12:45:27 +0000 Subject: [PATCH 61/89] release: clean up after v0.9.4 --- CHANGELOG.md | 2 ++ terraform/version.go | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d918b76c6..c537f5f1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## 0.9.5 (Unreleased) + ## 0.9.4 (26th April 2017) BACKWARDS INCOMPATIBILITIES / NOTES: diff --git a/terraform/version.go b/terraform/version.go index e184dc5a6..22e68c9ee 100644 --- a/terraform/version.go +++ b/terraform/version.go @@ -7,12 +7,12 @@ import ( ) // The main version number that is being run at the moment. -const Version = "0.9.4" +const Version = "0.9.5" // A pre-release marker for the version. If this is "" (empty string) // then it means that it is a final release. Otherwise, this is a pre-release // such as "dev" (in development), "beta", "rc1", etc. -const VersionPrerelease = "" +const VersionPrerelease = "dev" // SemVersion is an instance of version.Version. This has the secondary // benefit of verifying during tests and init time that our version is a From 7448e832b30a03c58be8c0a288646bb802580f40 Mon Sep 17 00:00:00 2001 From: stack72 Date: Thu, 27 Apr 2017 01:03:20 +1200 Subject: [PATCH 62/89] provider/alicloud: Migrating documentation from sidebar regex to strings --- website/source/layouts/alicloud.erb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/website/source/layouts/alicloud.erb b/website/source/layouts/alicloud.erb index f6695b85c..b8cd55db3 100644 --- a/website/source/layouts/alicloud.erb +++ b/website/source/layouts/alicloud.erb @@ -10,7 +10,7 @@ Alicloud Provider - > + > Data Sources - > + > ECS Resources - > + > SLB Resources - > + > VPC Resources - > + > RDS Resources - > + > ESS Resources