Merge branch 'master' into paddy_10984_better_image_resolution

This commit is contained in:
Paddy 2017-01-29 23:56:00 -08:00
commit 95e01ad35b
631 changed files with 138580 additions and 12731 deletions

View File

@ -11,7 +11,7 @@ install:
- bash scripts/gogetcookie.sh
- go get github.com/kardianos/govendor
script:
- make vendor-status vet test
- make vet test
- GOOS=windows go build
branches:
only:

View File

@ -3,31 +3,45 @@
BACKWARDS INCOMPATIBILITIES / NOTES:
* provider/aws: We no longer prefix an ECR repository address with `https://`
* provider/google: `google_project` has undergone significant changes. Existing configs and state should continue to work as they always have, but new configs and state will exhibit some new behaviour, including actually creating and deleting projects, instead of just referencing them. See https://www.terraform.io/docs/providers/google/r/google_project.html for more details.
FEATURES:
* **New Data Source:** `aws_autoscaling_groups` [GH-11303]
* **New Data Source:** `aws_elb_hosted_zone_id ` [GH-11027]
* **New Data Source:** `aws_instance` [GH-11272]
* **New Provider:** `ProfitBricks` [GH-7943]
* **New Data Source:** `aws_canonical_user_id` [GH-11332]
* **New Data Source:** `aws_vpc_endpoint` [GH-11323]
* **New Provider:** `profitbricks` [GH-7943]
* **New Provider:** `alicloud` [GH-11235]
* **New Provider:** `ns1` [GH-10782]
* **New Resource:** `aws_inspector_assessment_target` [GH-11217]
* **New Resource:** `aws_inspector_assessment_template` [GH-11217]
* **New Resource:** `aws_inspector_resource_group` [GH-11217]
* **New Resource:** `google_project_iam_policy` [GH-10425]
* **New Resource:** `google_project_services` [GH-10425]
* **New Interpolation Function:** `pathexpand()` [GH-11277]
IMPROVEMENTS:
* command/fmt: Single line objects (such as `variable "foo" {}`) aren't separated by newlines
* provider/aws: Add 'route_table_id' to route_table data source ([#11157](https://github.com/hashicorp/terraform/pull/11157))
* provider/aws: Add Support for aws_cloudwatch_metric_alarm extended statistic [GH-11193]
* provider/aws: Make the type of a route53_record modifiable without recreating the resource [GH-11164]
* provider/aws: Add Placement Strategy to aws_ecs_service resource [GH-11201]
* provider/aws: Add support for placement_constraint to aws_ecs_service [GH-11242]
* provider/aws: allow ALB target group stickiness to be enabled/disabled [GH-11251]
* provider/aws: ALBs now wait for provisioning to complete before proceeding [GH-11333]
* provider/aws: Add support for setting MSSQL Timezone in aws_db_instance [GH-11247]
* provider/aws: CloudFormation YAML template support [GH-11121]
* provider/aws: Remove hardcoded https from the ecr repository [GH-11307]
* provider/aws: implement CloudFront Lambda Function Associations [GH-11291]
* provider/aws: Implement CloudFront Lambda Function Associations [GH-11291]
* provider/aws: Remove MaxFrameRate default on ElasticTranscoderPreset [GH-11340]
* provider/aws: Allow ARN Identifier to be set for different partitions [GH-11359]
* provider/aws: Allow bypassing region validation [GH-11358]
* provider/aws: Added a s3_bucket domain name attribute [GH-10088]
* provider/aws: Add DiffSupressFunction to aws_db_instance's engine_version [GH-11369]
* provider/archive: Adding support for multiple source contents [GH-11271]
* provider/azurerm: add caching support for virtual_machine data_disks [GH-11142]
* provider/azurerm: make lb sub resources idempotent [GH-11128]
* provider/cloudflare: Add verification for record types and content [GH-11197]
@ -37,7 +51,9 @@ IMPROVEMENTS:
* provider/google: Add support for encrypting a disk [GH-11167]
* provider/google: Add support for session_affinity to google_compute_region_backend_service [GH-11228]
* provider/google: Allow additional zones to be configured in GKE [GH-11018]
* provider/ignition: Allow empty dropin and content for systemd_units [GH-11327]
* provider/openstack: LoadBalancer Security Groups [GH-11074]
* provider/openstack: Volume Attachment Updates [GH-11285]
* provider/scaleway improve bootscript data source [GH-11183]
* provider/statuscake: Add support for StatusCake confirmation servers [GH-11179]
* provider/statuscake: Add support for Updating StatusCake contact_ids [GH-7115]
@ -46,9 +62,13 @@ IMPROVEMENTS:
BUG FIXES:
* command/fmt: Multiple `#` comments won't be separated by newlines. [GH-11209]
* command/fmt: Lists with a heredoc element that starts on the same line as the opening brace is formatted properly. [GH-11208]
* provider/aws: Fix panic when querying VPC's main route table via data source ([#11134](https://github.com/hashicorp/terraform/issues/11134))
* provider/aws: Allow creating aws_codecommit repository outside of us-east-1 [GH-11177]
* provider/aws: Fix issue destroying or updating CloudFront due to missing Lambda Function Associations parameters [GH-11291]
* provider/aws: Correct error messages are now returned if an `aws_autoscaling_lifecycle_hook` fails during creation [GH-11360]
* provider/aws: Fix issue updating/destroying Spot Fleet requests when using `terminate_instances_with_expiration` [GH-10953]
* provider/azurerm: use configured environment for storage clients [GH-11159]
* provider/google: removes region param from google_compute_backend_service [GH-10903]
* provider/ignition: allowing empty systemd.content when a dropin is provided [GH-11216]

View File

@ -0,0 +1,12 @@
package main
import (
"github.com/hashicorp/terraform/builtin/providers/ns1"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: ns1.Provider,
})
}

View File

@ -0,0 +1 @@
package main

View File

@ -9,6 +9,7 @@ type Archiver interface {
ArchiveContent(content []byte, infilename string) error
ArchiveFile(infilename string) error
ArchiveDir(indirname string) error
ArchiveMultiple(content map[string][]byte) error
}
type ArchiverBuilder func(filepath string) Archiver

View File

@ -1,6 +1,7 @@
package archive
import (
"bytes"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
@ -11,6 +12,7 @@ import (
"os"
"path"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
)
@ -24,6 +26,33 @@ func dataSourceFile() *schema.Resource {
Required: true,
ForceNew: true,
},
"source": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"content": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"filename": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
},
ConflictsWith: []string{"source_file", "source_dir", "source_content", "source_content_filename"},
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["filename"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["content"].(string)))
return hashcode.String(buf.String())
},
},
"source_content": &schema.Schema{
Type: schema.TypeString,
Optional: true,
@ -138,6 +167,16 @@ func archive(d *schema.ResourceData) error {
if err := archiver.ArchiveContent([]byte(content), filename.(string)); err != nil {
return fmt.Errorf("error archiving content: %s", err)
}
} else if v, ok := d.GetOk("source"); ok {
vL := v.(*schema.Set).List()
content := make(map[string][]byte)
for _, v := range vL {
src := v.(map[string]interface{})
content[src["filename"].(string)] = []byte(src["content"].(string))
}
if err := archiver.ArchiveMultiple(content); err != nil {
return fmt.Errorf("error archiving content: %s", err)
}
} else {
return fmt.Errorf("one of 'source_dir', 'source_file', 'source_content_filename' must be specified")
}

View File

@ -51,6 +51,13 @@ func TestAccArchiveFile_Basic(t *testing.T) {
r.TestCheckResourceAttrPtr("data.archive_file.foo", "output_size", &fileSize),
),
},
r.TestStep{
Config: testAccArchiveFileMultiConfig,
Check: r.ComposeTestCheckFunc(
testAccArchiveFileExists("zip_file_acc_test.zip", &fileSize),
r.TestCheckResourceAttrPtr("data.archive_file.foo", "output_size", &fileSize),
),
},
r.TestStep{
Config: testAccArchiveFileOutputPath,
Check: r.ComposeTestCheckFunc(
@ -107,3 +114,14 @@ data "archive_file" "foo" {
output_path = "zip_file_acc_test.zip"
}
`
var testAccArchiveFileMultiConfig = `
data "archive_file" "foo" {
type = "zip"
source {
filename = "content.txt"
content = "This is some content"
}
output_path = "zip_file_acc_test.zip"
}
`

View File

@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"sort"
)
type ZipArchiver struct {
@ -85,6 +86,34 @@ func (a *ZipArchiver) ArchiveDir(indirname string) error {
}
func (a *ZipArchiver) ArchiveMultiple(content map[string][]byte) error {
if err := a.open(); err != nil {
return err
}
defer a.close()
// Ensure files are processed in the same order so hashes don't change
keys := make([]string, len(content))
i := 0
for k := range content {
keys[i] = k
i++
}
sort.Strings(keys)
for _, filename := range keys {
f, err := a.writer.Create(filename)
if err != nil {
return err
}
_, err = f.Write(content[filename])
if err != nil {
return err
}
}
return nil
}
func (a *ZipArchiver) open() error {
f, err := os.Create(a.filepath)
if err != nil {

View File

@ -44,6 +44,23 @@ func TestZipArchiver_Dir(t *testing.T) {
})
}
func TestZipArchiver_Multiple(t *testing.T) {
zipfilepath := "archive-content.zip"
content := map[string][]byte{
"file1.txt": []byte("This is file 1"),
"file2.txt": []byte("This is file 2"),
"file3.txt": []byte("This is file 3"),
}
archiver := NewZipArchiver(zipfilepath)
if err := archiver.ArchiveMultiple(content); err != nil {
t.Fatalf("unexpected error: %s", err)
}
ensureContents(t, zipfilepath, content)
}
func ensureContents(t *testing.T, zipfilepath string, wants map[string][]byte) {
r, err := zip.OpenReader(zipfilepath)
if err != nil {

View File

@ -92,6 +92,7 @@ type Config struct {
Insecure bool
SkipCredsValidation bool
SkipRegionValidation bool
SkipRequestingAccountId bool
SkipMetadataApiCheck bool
S3ForcePathStyle bool
@ -153,10 +154,14 @@ type AWSClient struct {
func (c *Config) Client() (interface{}, error) {
// Get the auth and region. This can fail if keys/regions were not
// specified and we're attempting to use the environment.
log.Println("[INFO] Building AWS region structure")
err := c.ValidateRegion()
if err != nil {
return nil, err
if c.SkipRegionValidation {
log.Println("[INFO] Skipping region validation")
} else {
log.Println("[INFO] Building AWS region structure")
err := c.ValidateRegion()
if err != nil {
return nil, err
}
}
var client AWSClient

View File

@ -74,7 +74,7 @@ func testAccDataSourceAWSALBListenerConfigBasic(albName, targetGroupName string)
resource "aws_alb" "alb_test" {
name = "%s"
internal = false
internal = true
security_groups = ["${aws_security_group.alb_test.id}"]
subnets = ["${aws_subnet.alb_test.*.id}"]
@ -221,6 +221,14 @@ resource "aws_vpc" "alb_test" {
}
}
resource "aws_internet_gateway" "gw" {
vpc_id = "${aws_vpc.alb_test.id}"
tags {
TestName = "TestAccAWSALB_basic"
}
}
resource "aws_subnet" "alb_test" {
count = 2
vpc_id = "${aws_vpc.alb_test.id}"

View File

@ -19,7 +19,7 @@ func TestAccDataSourceAWSALB_basic(t *testing.T) {
Config: testAccDataSourceAWSALBConfigBasic(albName),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.aws_alb.alb_test_with_arn", "name", albName),
resource.TestCheckResourceAttr("data.aws_alb.alb_test_with_arn", "internal", "false"),
resource.TestCheckResourceAttr("data.aws_alb.alb_test_with_arn", "internal", "true"),
resource.TestCheckResourceAttr("data.aws_alb.alb_test_with_arn", "subnets.#", "2"),
resource.TestCheckResourceAttr("data.aws_alb.alb_test_with_arn", "security_groups.#", "1"),
resource.TestCheckResourceAttr("data.aws_alb.alb_test_with_arn", "tags.%", "1"),
@ -31,7 +31,7 @@ func TestAccDataSourceAWSALB_basic(t *testing.T) {
resource.TestCheckResourceAttrSet("data.aws_alb.alb_test_with_arn", "dns_name"),
resource.TestCheckResourceAttrSet("data.aws_alb.alb_test_with_arn", "arn"),
resource.TestCheckResourceAttr("data.aws_alb.alb_test_with_name", "name", albName),
resource.TestCheckResourceAttr("data.aws_alb.alb_test_with_name", "internal", "false"),
resource.TestCheckResourceAttr("data.aws_alb.alb_test_with_name", "internal", "true"),
resource.TestCheckResourceAttr("data.aws_alb.alb_test_with_name", "subnets.#", "2"),
resource.TestCheckResourceAttr("data.aws_alb.alb_test_with_name", "security_groups.#", "1"),
resource.TestCheckResourceAttr("data.aws_alb.alb_test_with_name", "tags.%", "1"),
@ -51,7 +51,7 @@ func TestAccDataSourceAWSALB_basic(t *testing.T) {
func testAccDataSourceAWSALBConfigBasic(albName string) string {
return fmt.Sprintf(`resource "aws_alb" "alb_test" {
name = "%s"
internal = false
internal = true
security_groups = ["${aws_security_group.alb_test.id}"]
subnets = ["${aws_subnet.alb_test.*.id}"]

View File

@ -1,6 +1,8 @@
package aws
import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
)
@ -12,7 +14,7 @@ func dataSourceAwsBillingServiceAccount() *schema.Resource {
Read: dataSourceAwsBillingServiceAccountRead,
Schema: map[string]*schema.Schema{
"arn": &schema.Schema{
"arn": {
Type: schema.TypeString,
Computed: true,
},
@ -23,7 +25,7 @@ func dataSourceAwsBillingServiceAccount() *schema.Resource {
func dataSourceAwsBillingServiceAccountRead(d *schema.ResourceData, meta interface{}) error {
d.SetId(billingAccountId)
d.Set("arn", "arn:aws:iam::"+billingAccountId+":root")
d.Set("arn", fmt.Sprintf("arn:%s:iam::%s:root", meta.(*AWSClient).partition, billingAccountId))
return nil
}

View File

@ -11,7 +11,7 @@ func TestAccAWSBillingServiceAccount_basic(t *testing.T) {
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccCheckAwsBillingServiceAccountConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.aws_billing_service_account.main", "id", "386209384616"),

View File

@ -0,0 +1,48 @@
package aws
import (
"fmt"
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/hashicorp/terraform/helper/schema"
)
func dataSourceAwsCanonicalUserId() *schema.Resource {
return &schema.Resource{
Read: dataSourceAwsCanonicalUserIdRead,
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Computed: true,
},
"display_name": {
Type: schema.TypeString,
Computed: true,
},
},
}
}
func dataSourceAwsCanonicalUserIdRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).s3conn
log.Printf("[DEBUG] Listing S3 buckets.")
req := &s3.ListBucketsInput{}
resp, err := conn.ListBuckets(req)
if err != nil {
return err
}
if resp == nil || resp.Owner == nil {
return fmt.Errorf("no canonical user ID found")
}
d.SetId(aws.StringValue(resp.Owner.ID))
d.Set("id", resp.Owner.ID)
d.Set("display_name", resp.Owner.DisplayName)
return nil
}

View File

@ -0,0 +1,52 @@
// make testacc TEST=./builtin/providers/aws/ TESTARGS='-run=TestAccDataSourceAwsCanonicalUserId_'
package aws
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccDataSourceAwsCanonicalUserId_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccDataSourceAwsCanonicalUserIdConfig,
Check: resource.ComposeTestCheckFunc(
testAccDataSourceAwsCanonicalUserIdCheckExists("data.aws_canonical_user_id.current"),
),
},
},
})
}
func testAccDataSourceAwsCanonicalUserIdCheckExists(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Can't find Canonical User ID resource: %s", name)
}
if rs.Primary.Attributes["id"] == "" {
return fmt.Errorf("Missing Canonical User ID")
}
if rs.Primary.Attributes["display_name"] == "" {
return fmt.Errorf("Missing Display Name")
}
return nil
}
}
const testAccDataSourceAwsCanonicalUserIdConfig = `
provider "aws" {
region = "us-west-2"
}
data "aws_canonical_user_id" "current" { }
`

View File

@ -31,11 +31,11 @@ func dataSourceAwsElbServiceAccount() *schema.Resource {
Read: dataSourceAwsElbServiceAccountRead,
Schema: map[string]*schema.Schema{
"region": &schema.Schema{
"region": {
Type: schema.TypeString,
Optional: true,
},
"arn": &schema.Schema{
"arn": {
Type: schema.TypeString,
Computed: true,
},
@ -52,7 +52,7 @@ func dataSourceAwsElbServiceAccountRead(d *schema.ResourceData, meta interface{}
if accid, ok := elbAccountIdPerRegionMap[region]; ok {
d.SetId(accid)
d.Set("arn", "arn:aws:iam::"+accid+":root")
d.Set("arn", fmt.Sprintf("arn:%s:iam::%s:root", meta.(*AWSClient).partition, accid))
return nil
}

View File

@ -11,14 +11,14 @@ func TestAccAWSElbServiceAccount_basic(t *testing.T) {
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccCheckAwsElbServiceAccountConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.aws_elb_service_account.main", "id", "797873946194"),
resource.TestCheckResourceAttr("data.aws_elb_service_account.main", "arn", "arn:aws:iam::797873946194:root"),
),
},
resource.TestStep{
{
Config: testAccCheckAwsElbServiceAccountExplicitRegionConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.aws_elb_service_account.regional", "id", "156460612806"),

View File

@ -189,5 +189,9 @@ data "aws_route_table" "by_filter" {
name = "association.main"
values = ["true"]
}
filter {
name = "vpc-id"
values = ["vpc-6bd70802"]
}
}
`

View File

@ -39,7 +39,7 @@ func TestAccDataSourceAWSS3BucketObject_basic(t *testing.T) {
resource.TestCheckResourceAttr("data.aws_s3_bucket_object.obj", "etag", "b10a8db164e0754105b7a99be72e3fe5"),
resource.TestMatchResourceAttr("data.aws_s3_bucket_object.obj", "last_modified",
regexp.MustCompile("^[a-zA-Z]{3}, [0-9]+ [a-zA-Z]+ [0-9]{4} [0-9:]+ [A-Z]+$")),
resource.TestCheckResourceAttr("data.aws_s3_bucket_object.obj", "body", ""),
resource.TestCheckNoResourceAttr("data.aws_s3_bucket_object.obj", "body"),
),
},
},
@ -145,7 +145,7 @@ func TestAccDataSourceAWSS3BucketObject_allParams(t *testing.T) {
resource.TestMatchResourceAttr("data.aws_s3_bucket_object.obj", "last_modified",
regexp.MustCompile("^[a-zA-Z]{3}, [0-9]+ [a-zA-Z]+ [0-9]{4} [0-9:]+ [A-Z]+$")),
resource.TestMatchResourceAttr("data.aws_s3_bucket_object.obj", "version_id", regexp.MustCompile("^.{32}$")),
resource.TestCheckResourceAttr("data.aws_s3_bucket_object.obj", "body", ""),
resource.TestCheckNoResourceAttr("data.aws_s3_bucket_object.obj", "body"),
resource.TestCheckResourceAttr("data.aws_s3_bucket_object.obj", "cache_control", "no-cache"),
resource.TestCheckResourceAttr("data.aws_s3_bucket_object.obj", "content_disposition", "attachment"),
resource.TestCheckResourceAttr("data.aws_s3_bucket_object.obj", "content_encoding", "gzip"),

View File

@ -0,0 +1,103 @@
package aws
import (
"fmt"
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
)
func dataSourceAwsVpcEndpoint() *schema.Resource {
return &schema.Resource{
Read: dataSourceAwsVpcEndpointRead,
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"state": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"vpc_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"service_name": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"policy": {
Type: schema.TypeString,
Computed: true,
},
"route_table_ids": &schema.Schema{
Type: schema.TypeSet,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
},
}
}
func dataSourceAwsVpcEndpointRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
log.Printf("[DEBUG] Reading VPC Endpoints.")
req := &ec2.DescribeVpcEndpointsInput{}
if id, ok := d.GetOk("id"); ok {
req.VpcEndpointIds = aws.StringSlice([]string{id.(string)})
}
req.Filters = buildEC2AttributeFilterList(
map[string]string{
"vpc-endpoint-state": d.Get("state").(string),
"vpc-id": d.Get("vpc_id").(string),
"service-name": d.Get("service_name").(string),
},
)
if len(req.Filters) == 0 {
// Don't send an empty filters list; the EC2 API won't accept it.
req.Filters = nil
}
resp, err := conn.DescribeVpcEndpoints(req)
if err != nil {
return err
}
if resp == nil || len(resp.VpcEndpoints) == 0 {
return fmt.Errorf("no matching VPC endpoint found")
}
if len(resp.VpcEndpoints) > 1 {
return fmt.Errorf("multiple VPC endpoints matched; use additional constraints to reduce matches to a single VPC endpoint")
}
vpce := resp.VpcEndpoints[0]
policy, err := normalizeJsonString(*vpce.PolicyDocument)
if err != nil {
return errwrap.Wrapf("policy contains an invalid JSON: {{err}}", err)
}
d.SetId(aws.StringValue(vpce.VpcEndpointId))
d.Set("id", vpce.VpcEndpointId)
d.Set("state", vpce.State)
d.Set("vpc_id", vpce.VpcId)
d.Set("service_name", vpce.ServiceName)
d.Set("policy", policy)
if err := d.Set("route_table_ids", aws.StringValueSlice(vpce.RouteTableIds)); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,129 @@
// make testacc TEST=./builtin/providers/aws/ TESTARGS='-run=TestAccDataSourceAwsVpcEndpoint_'
package aws
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccDataSourceAwsVpcEndpoint_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDataSourceAwsVpcEndpointConfig,
Check: resource.ComposeTestCheckFunc(
testAccDataSourceAwsVpcEndpointCheckExists("data.aws_vpc_endpoint.s3"),
),
ExpectNonEmptyPlan: true,
},
},
})
}
func TestAccDataSourceAwsVpcEndpoint_withRouteTable(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDataSourceAwsVpcEndpointWithRouteTableConfig,
Check: resource.ComposeTestCheckFunc(
testAccDataSourceAwsVpcEndpointCheckExists("data.aws_vpc_endpoint.s3"),
resource.TestCheckResourceAttr(
"data.aws_vpc_endpoint.s3", "route_table_ids.#", "1"),
),
ExpectNonEmptyPlan: true,
},
},
})
}
func testAccDataSourceAwsVpcEndpointCheckExists(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("root module has no resource called %s", name)
}
vpceRs, ok := s.RootModule().Resources["aws_vpc_endpoint.s3"]
if !ok {
return fmt.Errorf("can't find aws_vpc_endpoint.s3 in state")
}
attr := rs.Primary.Attributes
if attr["id"] != vpceRs.Primary.Attributes["id"] {
return fmt.Errorf(
"id is %s; want %s",
attr["id"],
vpceRs.Primary.Attributes["id"],
)
}
return nil
}
}
const testAccDataSourceAwsVpcEndpointConfig = `
provider "aws" {
region = "us-west-2"
}
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
tags {
Name = "terraform-testacc-vpc-endpoint-data-source-foo"
}
}
resource "aws_vpc_endpoint" "s3" {
vpc_id = "${aws_vpc.foo.id}"
service_name = "com.amazonaws.us-west-2.s3"
}
data "aws_vpc_endpoint" "s3" {
vpc_id = "${aws_vpc.foo.id}"
service_name = "com.amazonaws.us-west-2.s3"
state = "available"
depends_on = ["aws_vpc_endpoint.s3"]
}
`
const testAccDataSourceAwsVpcEndpointWithRouteTableConfig = `
provider "aws" {
region = "us-west-2"
}
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
tags {
Name = "terraform-testacc-vpc-endpoint-data-source-foo"
}
}
resource "aws_route_table" "rt" {
vpc_id = "${aws_vpc.foo.id}"
}
resource "aws_vpc_endpoint" "s3" {
vpc_id = "${aws_vpc.foo.id}"
service_name = "com.amazonaws.us-west-2.s3"
route_table_ids = ["${aws_route_table.rt.id}"]
}
data "aws_vpc_endpoint" "s3" {
vpc_id = "${aws_vpc.foo.id}"
service_name = "com.amazonaws.us-west-2.s3"
state = "available"
depends_on = ["aws_vpc_endpoint.s3"]
}
`

View File

@ -1,6 +1,9 @@
package aws
import (
"log"
"strings"
"github.com/hashicorp/terraform/helper/schema"
"github.com/jen20/awspolicyequivalence"
)
@ -13,3 +16,18 @@ func suppressEquivalentAwsPolicyDiffs(k, old, new string, d *schema.ResourceData
return equivalent
}
// Suppresses minor version changes to the db_instance engine_version attribute
func suppressAwsDbEngineVersionDiffs(k, old, new string, d *schema.ResourceData) bool {
if d.Get("auto_minor_version_upgrade").(bool) {
// If we're set to auto upgrade minor versions
// ignore a minor version diff between versions
if strings.HasPrefix(old, new) {
log.Printf("[DEBUG] Ignoring minor version diff")
return true
}
}
// Throw a diff by default
return false
}

View File

@ -18,11 +18,11 @@ func TestAccAWSS3Bucket_importBasic(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSS3BucketDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSS3BucketConfig(rInt),
},
resource.TestStep{
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
@ -63,11 +63,11 @@ func TestAccAWSS3Bucket_importWithPolicy(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSS3BucketDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSS3BucketConfigWithPolicy(rInt),
},
resource.TestStep{
{
ResourceName: "aws_s3_bucket.bucket",
ImportState: true,
ImportStateCheck: checkFn,

View File

@ -120,6 +120,13 @@ func Provider() terraform.ResourceProvider {
Description: descriptions["skip_credentials_validation"],
},
"skip_region_validation": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: descriptions["skip_region_validation"],
},
"skip_requesting_account_id": {
Type: schema.TypeBool,
Optional: true,
@ -152,6 +159,7 @@ func Provider() terraform.ResourceProvider {
"aws_availability_zones": dataSourceAwsAvailabilityZones(),
"aws_billing_service_account": dataSourceAwsBillingServiceAccount(),
"aws_caller_identity": dataSourceAwsCallerIdentity(),
"aws_canonical_user_id": dataSourceAwsCanonicalUserId(),
"aws_cloudformation_stack": dataSourceAwsCloudFormationStack(),
"aws_ebs_snapshot": dataSourceAwsEbsSnapshot(),
"aws_ebs_volume": dataSourceAwsEbsVolume(),
@ -173,6 +181,7 @@ func Provider() terraform.ResourceProvider {
"aws_subnet": dataSourceAwsSubnet(),
"aws_security_group": dataSourceAwsSecurityGroup(),
"aws_vpc": dataSourceAwsVpc(),
"aws_vpc_endpoint": dataSourceAwsVpcEndpoint(),
"aws_vpc_endpoint_service": dataSourceAwsVpcEndpointService(),
"aws_vpc_peering_connection": dataSourceAwsVpcPeeringConnection(),
},
@ -441,6 +450,9 @@ func init() {
"skip_credentials_validation": "Skip the credentials validation via STS API. " +
"Used for AWS API implementations that do not have STS available/implemented.",
"skip_region_validation": "Skip static validation of region name. " +
"Used by users of alternative AWS-like APIs or users w/ access to regions that are not public (yet).",
"skip_requesting_account_id": "Skip requesting the account ID. " +
"Used for AWS API implementations that do not have IAM/STS API and/or metadata API.",
@ -475,6 +487,7 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
KinesisEndpoint: d.Get("kinesis_endpoint").(string),
Insecure: d.Get("insecure").(bool),
SkipCredsValidation: d.Get("skip_credentials_validation").(bool),
SkipRegionValidation: d.Get("skip_region_validation").(bool),
SkipRequestingAccountId: d.Get("skip_requesting_account_id").(bool),
SkipMetadataApiCheck: d.Get("skip_metadata_api_check").(bool),
S3ForcePathStyle: d.Get("s3_force_path_style").(bool),

View File

@ -5,6 +5,7 @@ import (
"log"
"regexp"
"strconv"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/elbv2"
@ -169,9 +170,38 @@ func resourceAwsAlbCreate(d *schema.ResourceData, meta interface{}) error {
return fmt.Errorf("No load balancers returned following creation of %s", d.Get("name").(string))
}
d.SetId(*resp.LoadBalancers[0].LoadBalancerArn)
lb := resp.LoadBalancers[0]
d.SetId(*lb.LoadBalancerArn)
log.Printf("[INFO] ALB ID: %s", d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"active", "provisioning", "failed"},
Target: []string{"active"},
Refresh: func() (interface{}, string, error) {
describeResp, err := elbconn.DescribeLoadBalancers(&elbv2.DescribeLoadBalancersInput{
LoadBalancerArns: []*string{lb.LoadBalancerArn},
})
if err != nil {
return nil, "", err
}
if len(describeResp.LoadBalancers) != 1 {
return nil, "", fmt.Errorf("No load balancers returned for %s", *lb.LoadBalancerArn)
}
dLb := describeResp.LoadBalancers[0]
log.Printf("[INFO] ALB state: %s", *dLb.State.Code)
return describeResp, *dLb.State.Code, nil
},
Timeout: 5 * time.Minute,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return err
}
return resourceAwsAlbUpdate(d, meta)
}

View File

@ -133,7 +133,7 @@ resource "aws_alb_listener" "front_end" {
resource "aws_alb" "alb_test" {
name = "%s"
internal = false
internal = true
security_groups = ["${aws_security_group.alb_test.id}"]
subnets = ["${aws_subnet.alb_test.*.id}"]

View File

@ -148,7 +148,7 @@ func testAccAWSALBListenerConfig_basic(albName, targetGroupName string) string {
resource "aws_alb" "alb_test" {
name = "%s"
internal = false
internal = true
security_groups = ["${aws_security_group.alb_test.id}"]
subnets = ["${aws_subnet.alb_test.*.id}"]
@ -291,6 +291,14 @@ resource "aws_vpc" "alb_test" {
}
}
resource "aws_internet_gateway" "gw" {
vpc_id = "${aws_vpc.alb_test.id}"
tags {
TestName = "TestAccAWSALB_basic"
}
}
resource "aws_subnet" "alb_test" {
count = 2
vpc_id = "${aws_vpc.alb_test.id}"

View File

@ -60,7 +60,7 @@ func TestAccAWSALB_basic(t *testing.T) {
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSALBExists("aws_alb.alb_test", &conf),
resource.TestCheckResourceAttr("aws_alb.alb_test", "name", albName),
resource.TestCheckResourceAttr("aws_alb.alb_test", "internal", "false"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "internal", "true"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "subnets.#", "2"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "security_groups.#", "1"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "tags.%", "1"),
@ -197,7 +197,7 @@ func TestAccAWSALB_noSecurityGroup(t *testing.T) {
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSALBExists("aws_alb.alb_test", &conf),
resource.TestCheckResourceAttr("aws_alb.alb_test", "name", albName),
resource.TestCheckResourceAttr("aws_alb.alb_test", "internal", "false"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "internal", "true"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "subnets.#", "2"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "security_groups.#", "1"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "tags.%", "1"),
@ -229,7 +229,7 @@ func TestAccAWSALB_accesslogs(t *testing.T) {
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSALBExists("aws_alb.alb_test", &conf),
resource.TestCheckResourceAttr("aws_alb.alb_test", "name", albName),
resource.TestCheckResourceAttr("aws_alb.alb_test", "internal", "false"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "internal", "true"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "subnets.#", "2"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "security_groups.#", "1"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "tags.%", "1"),
@ -247,7 +247,7 @@ func TestAccAWSALB_accesslogs(t *testing.T) {
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSALBExists("aws_alb.alb_test", &conf),
resource.TestCheckResourceAttr("aws_alb.alb_test", "name", albName),
resource.TestCheckResourceAttr("aws_alb.alb_test", "internal", "false"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "internal", "true"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "subnets.#", "2"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "security_groups.#", "1"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "tags.%", "1"),
@ -269,7 +269,7 @@ func TestAccAWSALB_accesslogs(t *testing.T) {
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSALBExists("aws_alb.alb_test", &conf),
resource.TestCheckResourceAttr("aws_alb.alb_test", "name", albName),
resource.TestCheckResourceAttr("aws_alb.alb_test", "internal", "false"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "internal", "true"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "subnets.#", "2"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "security_groups.#", "1"),
resource.TestCheckResourceAttr("aws_alb.alb_test", "tags.%", "1"),
@ -362,7 +362,7 @@ func testAccCheckAWSALBDestroy(s *terraform.State) error {
func testAccAWSALBConfig_basic(albName string) string {
return fmt.Sprintf(`resource "aws_alb" "alb_test" {
name = "%s"
internal = false
internal = true
security_groups = ["${aws_security_group.alb_test.id}"]
subnets = ["${aws_subnet.alb_test.*.id}"]
@ -429,7 +429,7 @@ resource "aws_security_group" "alb_test" {
func testAccAWSALBConfig_generatedName() string {
return fmt.Sprintf(`
resource "aws_alb" "alb_test" {
internal = false
internal = true
security_groups = ["${aws_security_group.alb_test.id}"]
subnets = ["${aws_subnet.alb_test.*.id}"]
@ -456,6 +456,14 @@ resource "aws_vpc" "alb_test" {
}
}
resource "aws_internet_gateway" "gw" {
vpc_id = "${aws_vpc.alb_test.id}"
tags {
Name = "TestAccAWSALB_basic"
}
}
resource "aws_subnet" "alb_test" {
count = 2
vpc_id = "${aws_vpc.alb_test.id}"
@ -497,7 +505,7 @@ func testAccAWSALBConfig_namePrefix() string {
return fmt.Sprintf(`
resource "aws_alb" "alb_test" {
name_prefix = "tf-lb"
internal = false
internal = true
security_groups = ["${aws_security_group.alb_test.id}"]
subnets = ["${aws_subnet.alb_test.*.id}"]
@ -563,7 +571,7 @@ resource "aws_security_group" "alb_test" {
func testAccAWSALBConfig_updatedTags(albName string) string {
return fmt.Sprintf(`resource "aws_alb" "alb_test" {
name = "%s"
internal = false
internal = true
security_groups = ["${aws_security_group.alb_test.id}"]
subnets = ["${aws_subnet.alb_test.*.id}"]
@ -631,7 +639,7 @@ resource "aws_security_group" "alb_test" {
func testAccAWSALBConfig_accessLogs(enabled bool, albName, bucketName string) string {
return fmt.Sprintf(`resource "aws_alb" "alb_test" {
name = "%s"
internal = false
internal = true
security_groups = ["${aws_security_group.alb_test.id}"]
subnets = ["${aws_subnet.alb_test.*.id}"]
@ -742,7 +750,7 @@ resource "aws_security_group" "alb_test" {
func testAccAWSALBConfig_nosg(albName string) string {
return fmt.Sprintf(`resource "aws_alb" "alb_test" {
name = "%s"
internal = false
internal = true
subnets = ["${aws_subnet.alb_test.*.id}"]
idle_timeout = 30
@ -784,7 +792,7 @@ resource "aws_subnet" "alb_test" {
func testAccAWSALBConfig_updateSecurityGroups(albName string) string {
return fmt.Sprintf(`resource "aws_alb" "alb_test" {
name = "%s"
internal = false
internal = true
security_groups = ["${aws_security_group.alb_test.id}", "${aws_security_group.alb_test_2.id}"]
subnets = ["${aws_subnet.alb_test.*.id}"]

View File

@ -28,8 +28,8 @@ func TestAccAWSAPIGatewayIntegrationResponse_basic(t *testing.T) {
"aws_api_gateway_integration_response.test", "response_templates.application/json", ""),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration_response.test", "response_templates.application/xml", "#set($inputRoot = $input.path('$'))\n{ }"),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration_response.test", "content_handling", ""),
resource.TestCheckNoResourceAttr(
"aws_api_gateway_integration_response.test", "content_handling"),
),
},

View File

@ -36,8 +36,8 @@ func TestAccAWSAPIGatewayIntegration_basic(t *testing.T) {
"aws_api_gateway_integration.test", "request_templates.application/xml", "#set($inputRoot = $input.path('$'))\n{ }"),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration.test", "passthrough_behavior", "WHEN_NO_MATCH"),
resource.TestCheckResourceAttr(
"aws_api_gateway_integration.test", "content_handling", ""),
resource.TestCheckNoResourceAttr(
"aws_api_gateway_integration.test", "content_handling"),
),
},

View File

@ -29,8 +29,8 @@ func TestAccAWSAPIGatewayRestApi_basic(t *testing.T) {
"aws_api_gateway_rest_api.test", "description", ""),
resource.TestCheckResourceAttrSet(
"aws_api_gateway_rest_api.test", "created_date"),
resource.TestCheckResourceAttr(
"aws_api_gateway_rest_api.test", "binary_media_types", ""),
resource.TestCheckNoResourceAttr(
"aws_api_gateway_rest_api.test", "binary_media_types"),
),
},

View File

@ -277,8 +277,8 @@ func TestAccAWSAutoScalingGroup_enablingMetrics(t *testing.T) {
Config: testAccAWSAutoScalingGroupConfig(randName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSAutoScalingGroupExists("aws_autoscaling_group.bar", &group),
resource.TestCheckResourceAttr(
"aws_autoscaling_group.bar", "enabled_metrics.#", ""),
resource.TestCheckNoResourceAttr(
"aws_autoscaling_group.bar", "enabled_metrics"),
),
},

View File

@ -1,7 +1,6 @@
package aws
import (
"fmt"
"log"
"strings"
"time"
@ -9,6 +8,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
@ -67,10 +67,10 @@ func resourceAwsAutoscalingLifecycleHookPutOp(conn *autoscaling.AutoScaling, par
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
if strings.Contains(awsErr.Message(), "Unable to publish test message to notification target") {
return resource.RetryableError(fmt.Errorf("[DEBUG] Retrying AWS AutoScaling Lifecycle Hook: %s", params))
return resource.RetryableError(errwrap.Wrapf("[DEBUG] Retrying AWS AutoScaling Lifecycle Hook: {{err}}", awsErr))
}
}
return resource.NonRetryableError(fmt.Errorf("Error putting lifecycle hook: %s", err))
return resource.NonRetryableError(errwrap.Wrapf("Error putting lifecycle hook: {{err}}", err))
}
return nil
})
@ -127,7 +127,7 @@ func resourceAwsAutoscalingLifecycleHookDelete(d *schema.ResourceData, meta inte
LifecycleHookName: aws.String(d.Get("name").(string)),
}
if _, err := autoscalingconn.DeleteLifecycleHook(&params); err != nil {
return fmt.Errorf("Autoscaling Lifecycle Hook: %s ", err)
return errwrap.Wrapf("Autoscaling Lifecycle Hook: {{err}}", err)
}
d.SetId("")
@ -178,7 +178,7 @@ func getAwsAutoscalingLifecycleHook(d *schema.ResourceData, meta interface{}) (*
log.Printf("[DEBUG] AutoScaling Lifecycle Hook Describe Params: %#v", params)
resp, err := autoscalingconn.DescribeLifecycleHooks(&params)
if err != nil {
return nil, fmt.Errorf("Error retrieving lifecycle hooks: %s", err)
return nil, errwrap.Wrapf("Error retrieving lifecycle hooks: {{err}}", err)
}
// find lifecycle hooks

View File

@ -20,28 +20,28 @@ func resourceAwsCloudFrontOriginAccessIdentity() *schema.Resource {
},
Schema: map[string]*schema.Schema{
"comment": &schema.Schema{
"comment": {
Type: schema.TypeString,
Optional: true,
Default: "",
},
"caller_reference": &schema.Schema{
"caller_reference": {
Type: schema.TypeString,
Computed: true,
},
"cloudfront_access_identity_path": &schema.Schema{
"cloudfront_access_identity_path": {
Type: schema.TypeString,
Computed: true,
},
"etag": &schema.Schema{
"etag": {
Type: schema.TypeString,
Computed: true,
},
"iam_arn": &schema.Schema{
"iam_arn": {
Type: schema.TypeString,
Computed: true,
},
"s3_canonical_user_id": &schema.Schema{
"s3_canonical_user_id": {
Type: schema.TypeString,
Computed: true,
},
@ -81,7 +81,8 @@ func resourceAwsCloudFrontOriginAccessIdentityRead(d *schema.ResourceData, meta
d.Set("etag", resp.ETag)
d.Set("s3_canonical_user_id", resp.CloudFrontOriginAccessIdentity.S3CanonicalUserId)
d.Set("cloudfront_access_identity_path", fmt.Sprintf("origin-access-identity/cloudfront/%s", *resp.CloudFrontOriginAccessIdentity.Id))
d.Set("iam_arn", fmt.Sprintf("arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity %s", *resp.CloudFrontOriginAccessIdentity.Id))
d.Set("iam_arn", fmt.Sprintf("arn:%s:iam::cloudfront:user/CloudFront Origin Access Identity %s",
meta.(*AWSClient).partition, *resp.CloudFrontOriginAccessIdentity.Id))
return nil
}

View File

@ -17,7 +17,7 @@ func TestAccAWSCloudFrontOriginAccessIdentity_basic(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudFrontOriginAccessIdentityDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSCloudFrontOriginAccessIdentityConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudFrontOriginAccessIdentityExistence("aws_cloudfront_origin_access_identity.origin_access_identity"),
@ -46,7 +46,7 @@ func TestAccAWSCloudFrontOriginAccessIdentity_noComment(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudFrontOriginAccessIdentityDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSCloudFrontOriginAccessIdentityNoCommentConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudFrontOriginAccessIdentityExistence("aws_cloudfront_origin_access_identity.origin_access_identity"),

View File

@ -86,8 +86,8 @@ func TestAccAWSCodeCommitRepository_create_and_update_default_branch(t *testing.
Config: testAccCodeCommitRepository_basic(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckCodeCommitRepositoryExists("aws_codecommit_repository.test"),
resource.TestCheckResourceAttr(
"aws_codecommit_repository.test", "default_branch", ""),
resource.TestCheckNoResourceAttr(
"aws_codecommit_repository.test", "default_branch"),
),
},
resource.TestStep{

View File

@ -63,9 +63,10 @@ func resourceAwsDbInstance() *schema.Resource {
},
"engine_version": {
Type: schema.TypeString,
Optional: true,
Computed: true,
Type: schema.TypeString,
Optional: true,
Computed: true,
DiffSuppressFunc: suppressAwsDbEngineVersionDiffs,
},
"character_set_name": {

View File

@ -27,7 +27,7 @@ func TestAccAWSDBInstance_basic(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSDBInstanceConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.bar", &v),
@ -65,7 +65,7 @@ func TestAccAWSDBInstance_kmsKey(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.bar", &v),
@ -115,7 +115,7 @@ func TestAccAWSDBInstance_optionGroup(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSDBInstanceConfigWithOptionGroup,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.bar", &v),
@ -136,7 +136,7 @@ func TestAccAWSDBInstanceReplica(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccReplicaInstanceConfig(rand.New(rand.NewSource(time.Now().UnixNano())).Int()),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.bar", &s),
@ -158,7 +158,7 @@ func TestAccAWSDBInstanceSnapshot(t *testing.T) {
// created, and subequently deletes it
CheckDestroy: testAccCheckAWSDBInstanceSnapshot,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccSnapshotInstanceConfig(),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.snapshot", &snap),
@ -176,7 +176,7 @@ func TestAccAWSDBInstanceNoSnapshot(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceNoSnapshot,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccNoSnapshotInstanceConfig(),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.no_snapshot", &nosnap),
@ -195,7 +195,7 @@ func TestAccAWSDBInstance_enhancedMonitoring(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceNoSnapshot,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccSnapshotInstanceConfig_enhancedMonitoring(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.enhanced_monitoring", &dbInstance),
@ -220,7 +220,7 @@ func TestAccAWS_separate_DBInstance_iops_update(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccSnapshotInstanceConfig_iopsUpdate(rName, 1000),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.bar", &v),
@ -228,7 +228,7 @@ func TestAccAWS_separate_DBInstance_iops_update(t *testing.T) {
),
},
resource.TestStep{
{
Config: testAccSnapshotInstanceConfig_iopsUpdate(rName, 2000),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.bar", &v),
@ -249,7 +249,7 @@ func TestAccAWSDBInstance_portUpdate(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccSnapshotInstanceConfig_mysqlPort(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.bar", &v),
@ -258,7 +258,7 @@ func TestAccAWSDBInstance_portUpdate(t *testing.T) {
),
},
resource.TestStep{
{
Config: testAccSnapshotInstanceConfig_updateMysqlPort(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.bar", &v),
@ -278,7 +278,7 @@ func TestAccAWSDBInstance_MSSQL_TZ(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSDBMSSQL_timezone,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.mssql", &v),
@ -290,7 +290,7 @@ func TestAccAWSDBInstance_MSSQL_TZ(t *testing.T) {
),
},
resource.TestStep{
{
Config: testAccAWSDBMSSQL_timezone_AKST,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.mssql", &v),
@ -305,6 +305,24 @@ func TestAccAWSDBInstance_MSSQL_TZ(t *testing.T) {
})
}
func TestAccAWSDBInstance_MinorVersion(t *testing.T) {
var v rds.DBInstance
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSDBInstanceConfigAutoMinorVersion,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists("aws_db_instance.bar", &v),
),
},
},
})
}
func testAccCheckAWSDBInstanceDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).rdsconn
@ -1113,3 +1131,16 @@ resource "aws_security_group_rule" "rds-mssql-1" {
security_group_id = "${aws_security_group.rds-mssql.id}"
}
`
var testAccAWSDBInstanceConfigAutoMinorVersion = fmt.Sprintf(`
resource "aws_db_instance" "bar" {
identifier = "foobarbaz-test-terraform-%d"
allocated_storage = 10
engine = "MySQL"
engine_version = "5.6"
instance_class = "db.t1.micro"
name = "baz"
password = "barbarbarbar"
username = "foo"
}
`, acctest.RandInt())

View File

@ -282,7 +282,7 @@ func resourceAwsEcsServiceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("name", service.ServiceName)
// Save task definition in the same format
if strings.HasPrefix(d.Get("task_definition").(string), "arn:aws:ecs:") {
if strings.HasPrefix(d.Get("task_definition").(string), "arn:"+meta.(*AWSClient).partition+":ecs:") {
d.Set("task_definition", service.TaskDefinition)
} else {
taskDefinition := buildFamilyAndRevisionFromARN(*service.TaskDefinition)
@ -292,7 +292,7 @@ func resourceAwsEcsServiceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("desired_count", service.DesiredCount)
// Save cluster in the same format
if strings.HasPrefix(d.Get("cluster").(string), "arn:aws:ecs:") {
if strings.HasPrefix(d.Get("cluster").(string), "arn:"+meta.(*AWSClient).partition+":ecs:") {
d.Set("cluster", service.ClusterArn)
} else {
clusterARN := getNameFromARN(*service.ClusterArn)
@ -301,7 +301,7 @@ func resourceAwsEcsServiceRead(d *schema.ResourceData, meta interface{}) error {
// Save IAM role in the same format
if service.RoleArn != nil {
if strings.HasPrefix(d.Get("iam_role").(string), "arn:aws:iam:") {
if strings.HasPrefix(d.Get("iam_role").(string), "arn:"+meta.(*AWSClient).partition+":iam:") {
d.Set("iam_role", service.RoleArn)
} else {
roleARN := getNameFromARN(*service.RoleArn)

View File

@ -915,6 +915,7 @@ resource "aws_alb_target_group" "test" {
resource "aws_alb" "main" {
name = "tf-acc-test-test-alb-ecs"
internal = true
subnets = ["${aws_subnet.main.*.id}"]
}

View File

@ -212,7 +212,6 @@ func resourceAwsElasticTranscoderPreset() *schema.Resource {
},
"max_frame_rate": &schema.Schema{
Type: schema.TypeString,
Default: "30",
Optional: true,
ForceNew: true,
},

View File

@ -51,8 +51,8 @@ func TestAccAWSAccessKey_encrypted(t *testing.T) {
testAccCheckAWSAccessKeyExists("aws_iam_access_key.a_key", &conf),
testAccCheckAWSAccessKeyAttributes(&conf),
testDecryptSecretKeyAndTest("aws_iam_access_key.a_key", testPrivKey1),
resource.TestCheckResourceAttr(
"aws_iam_access_key.a_key", "secret", ""),
resource.TestCheckNoResourceAttr(
"aws_iam_access_key.a_key", "secret"),
resource.TestCheckResourceAttrSet(
"aws_iam_access_key.a_key", "encrypted_secret"),
resource.TestCheckResourceAttrSet(

View File

@ -23,20 +23,20 @@ func resourceAwsIamSamlProvider() *schema.Resource {
},
Schema: map[string]*schema.Schema{
"arn": &schema.Schema{
"arn": {
Type: schema.TypeString,
Computed: true,
},
"valid_until": &schema.Schema{
"valid_until": {
Type: schema.TypeString,
Computed: true,
},
"name": &schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"saml_metadata_document": &schema.Schema{
"saml_metadata_document": {
Type: schema.TypeString,
Required: true,
},
@ -75,7 +75,7 @@ func resourceAwsIamSamlProviderRead(d *schema.ResourceData, meta interface{}) er
validUntil := out.ValidUntil.Format(time.RFC1123)
d.Set("arn", d.Id())
name, err := extractNameFromIAMSamlProviderArn(d.Id())
name, err := extractNameFromIAMSamlProviderArn(d.Id(), meta.(*AWSClient).partition)
if err != nil {
return err
}
@ -112,9 +112,9 @@ func resourceAwsIamSamlProviderDelete(d *schema.ResourceData, meta interface{})
return err
}
func extractNameFromIAMSamlProviderArn(arn string) (string, error) {
func extractNameFromIAMSamlProviderArn(arn, partition string) (string, error) {
// arn:aws:iam::123456789012:saml-provider/tf-salesforce-test
r := regexp.MustCompile("^arn:aws:iam::[0-9]{12}:saml-provider/(.+)$")
r := regexp.MustCompile(fmt.Sprintf("^arn:%s:iam::[0-9]{12}:saml-provider/(.+)$", partition))
submatches := r.FindStringSubmatch(arn)
if len(submatches) != 2 {
return "", fmt.Errorf("Unable to extract name from a given ARN: %q", arn)

View File

@ -17,13 +17,13 @@ func TestAccAWSIAMSamlProvider_basic(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckIAMSamlProviderDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccIAMSamlProviderConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckIAMSamlProvider("aws_iam_saml_provider.salesforce"),
),
},
resource.TestStep{
{
Config: testAccIAMSamlProviderConfigUpdate,
Check: resource.ComposeTestCheckFunc(
testAccCheckIAMSamlProvider("aws_iam_saml_provider.salesforce"),

View File

@ -134,8 +134,8 @@ func TestAccAWSKinesisStream_shardLevelMetrics(t *testing.T) {
Check: resource.ComposeTestCheckFunc(
testAccCheckKinesisStreamExists("aws_kinesis_stream.test_stream", &stream),
testAccCheckAWSKinesisStreamAttributes(&stream),
resource.TestCheckResourceAttr(
"aws_kinesis_stream.test_stream", "shard_level_metrics.#", ""),
resource.TestCheckNoResourceAttr(
"aws_kinesis_stream.test_stream", "shard_level_metrics"),
),
},

View File

@ -74,7 +74,7 @@ func TestAccAWSLambdaFunction_envVariables(t *testing.T) {
testAccCheckAwsLambdaFunctionExists("aws_lambda_function.lambda_function_test", rName, &conf),
testAccCheckAwsLambdaFunctionName(&conf, rName),
testAccCheckAwsLambdaFunctionArnHasSuffix(&conf, ":"+rName),
resource.TestCheckResourceAttr("aws_lambda_function.lambda_function_test", "environment.#", "0"),
resource.TestCheckNoResourceAttr("aws_lambda_function.lambda_function_test", "environment"),
),
},
{
@ -102,8 +102,7 @@ func TestAccAWSLambdaFunction_envVariables(t *testing.T) {
testAccCheckAwsLambdaFunctionExists("aws_lambda_function.lambda_function_test", rName, &conf),
testAccCheckAwsLambdaFunctionName(&conf, rName),
testAccCheckAwsLambdaFunctionArnHasSuffix(&conf, ":"+rName),
resource.TestCheckResourceAttr("aws_lambda_function.lambda_function_test", "environment.0.variables.foo", ""),
resource.TestCheckResourceAttr("aws_lambda_function.lambda_function_test", "environment.0.variables.foo1", ""),
resource.TestCheckNoResourceAttr("aws_lambda_function.lambda_function_test", "environment"),
),
},
},

View File

@ -15,7 +15,7 @@ import (
"github.com/hashicorp/terraform/helper/schema"
)
var LambdaFunctionRegexp = `^(arn:aws:lambda:)?([a-z]{2}-[a-z]+-\d{1}:)?(\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\$LATEST|[a-zA-Z0-9-_]+))?$`
var LambdaFunctionRegexp = `^(arn:[\w-]+:lambda:)?([a-z]{2}-[a-z]+-\d{1}:)?(\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\$LATEST|[a-zA-Z0-9-_]+))?$`
func resourceAwsLambdaPermission() *schema.Resource {
return &schema.Resource{
@ -24,42 +24,42 @@ func resourceAwsLambdaPermission() *schema.Resource {
Delete: resourceAwsLambdaPermissionDelete,
Schema: map[string]*schema.Schema{
"action": &schema.Schema{
"action": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateLambdaPermissionAction,
},
"function_name": &schema.Schema{
"function_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateLambdaFunctionName,
},
"principal": &schema.Schema{
"principal": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"qualifier": &schema.Schema{
"qualifier": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validateLambdaQualifier,
},
"source_account": &schema.Schema{
"source_account": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validateAwsAccountId,
},
"source_arn": &schema.Schema{
"source_arn": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validateArn,
},
"statement_id": &schema.Schema{
"statement_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
@ -216,7 +216,7 @@ func resourceAwsLambdaPermissionRead(d *schema.ResourceData, meta interface{}) e
}
// Save Lambda function name in the same format
if strings.HasPrefix(d.Get("function_name").(string), "arn:aws:lambda:") {
if strings.HasPrefix(d.Get("function_name").(string), "arn:"+meta.(*AWSClient).partition+":lambda:") {
// Strip qualifier off
trimmedArn := strings.TrimSuffix(statement.Resource, ":"+qualifier)
d.Set("function_name", trimmedArn)

View File

@ -63,6 +63,18 @@ func TestLambdaPermissionGetQualifierFromLambdaAliasOrVersionArn_alias(t *testin
}
}
func TestLambdaPermissionGetQualifierFromLambdaAliasOrVersionArn_govcloud(t *testing.T) {
arnWithAlias := "arn:aws-us-gov:lambda:us-west-2:187636751137:function:lambda_function_name:testalias"
expectedQualifier := "testalias"
qualifier, err := getQualifierFromLambdaAliasOrVersionArn(arnWithAlias)
if err != nil {
t.Fatalf("Expected no error when getting qualifier: %s", err)
}
if qualifier != expectedQualifier {
t.Fatalf("Expected qualifier to match (%q != %q)", qualifier, expectedQualifier)
}
}
func TestLambdaPermissionGetQualifierFromLambdaAliasOrVersionArn_version(t *testing.T) {
arnWithVersion := "arn:aws:lambda:us-west-2:187636751137:function:lambda_function_name:223"
expectedQualifier := "223"
@ -141,7 +153,7 @@ func TestAccAWSLambdaPermission_basic(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLambdaPermissionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSLambdaPermissionConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckLambdaPermissionExists("aws_lambda_permission.allow_cloudwatch", &statement),
@ -165,7 +177,7 @@ func TestAccAWSLambdaPermission_withRawFunctionName(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLambdaPermissionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSLambdaPermissionConfig_withRawFunctionName,
Check: resource.ComposeTestCheckFunc(
testAccCheckLambdaPermissionExists("aws_lambda_permission.with_raw_func_name", &statement),
@ -188,7 +200,7 @@ func TestAccAWSLambdaPermission_withQualifier(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLambdaPermissionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSLambdaPermissionConfig_withQualifier,
Check: resource.ComposeTestCheckFunc(
testAccCheckLambdaPermissionExists("aws_lambda_permission.with_qualifier", &statement),
@ -217,7 +229,7 @@ func TestAccAWSLambdaPermission_multiplePerms(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLambdaPermissionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSLambdaPermissionConfig_multiplePerms,
Check: resource.ComposeTestCheckFunc(
// 1st
@ -236,7 +248,7 @@ func TestAccAWSLambdaPermission_multiplePerms(t *testing.T) {
regexp.MustCompile(":function:lambda_function_name_perm_multiperms$")),
),
},
resource.TestStep{
{
Config: testAccAWSLambdaPermissionConfig_multiplePermsModified,
Check: resource.ComposeTestCheckFunc(
// 1st
@ -277,7 +289,7 @@ func TestAccAWSLambdaPermission_withS3(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLambdaPermissionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: fmt.Sprintf(testAccAWSLambdaPermissionConfig_withS3_tpl, rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckLambdaPermissionExists("aws_lambda_permission.with_s3", &statement),
@ -303,7 +315,7 @@ func TestAccAWSLambdaPermission_withSNS(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLambdaPermissionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSLambdaPermissionConfig_withSNS,
Check: resource.ComposeTestCheckFunc(
testAccCheckLambdaPermissionExists("aws_lambda_permission.with_sns", &statement),

View File

@ -52,9 +52,9 @@ func TestAccAWSLightsailKeyPair_imported(t *testing.T) {
resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "arn"),
resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "fingerprint"),
resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "public_key"),
resource.TestCheckResourceAttr("aws_lightsail_key_pair.lightsail_key_pair_test", "encrypted_fingerprint", ""),
resource.TestCheckResourceAttr("aws_lightsail_key_pair.lightsail_key_pair_test", "encrypted_private_key", ""),
resource.TestCheckResourceAttr("aws_lightsail_key_pair.lightsail_key_pair_test", "private_key", ""),
resource.TestCheckNoResourceAttr("aws_lightsail_key_pair.lightsail_key_pair_test", "encrypted_fingerprint"),
resource.TestCheckNoResourceAttr("aws_lightsail_key_pair.lightsail_key_pair_test", "encrypted_private_key"),
resource.TestCheckNoResourceAttr("aws_lightsail_key_pair.lightsail_key_pair_test", "private_key"),
),
},
},
@ -79,7 +79,7 @@ func TestAccAWSLightsailKeyPair_encrypted(t *testing.T) {
resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "encrypted_fingerprint"),
resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "encrypted_private_key"),
resource.TestCheckResourceAttrSet("aws_lightsail_key_pair.lightsail_key_pair_test", "public_key"),
resource.TestCheckResourceAttr("aws_lightsail_key_pair.lightsail_key_pair_test", "private_key", ""),
resource.TestCheckNoResourceAttr("aws_lightsail_key_pair.lightsail_key_pair_test", "private_key"),
),
},
},

View File

@ -36,6 +36,11 @@ func resourceAwsS3Bucket() *schema.Resource {
ForceNew: true,
},
"bucket_domain_name": {
Type: schema.TypeString,
Computed: true,
},
"arn": {
Type: schema.TypeString,
Optional: true,
@ -534,6 +539,8 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error {
d.Set("bucket", d.Id())
}
d.Set("bucket_domain_name", bucketDomainName(d.Get("bucket").(string)))
// Read the policy
if _, ok := d.GetOk("policy"); ok {
pol, err := s3conn.GetBucketPolicy(&s3.GetBucketPolicyInput{
@ -905,7 +912,7 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error {
return err
}
d.Set("arn", fmt.Sprint("arn:aws:s3:::", d.Id()))
d.Set("arn", fmt.Sprintf("arn:%s:s3:::%s", meta.(*AWSClient).partition, d.Id()))
return nil
}
@ -1207,6 +1214,10 @@ func websiteEndpoint(s3conn *s3.S3, d *schema.ResourceData) (*S3Website, error)
return WebsiteEndpoint(bucket, region), nil
}
func bucketDomainName(bucket string) string {
return fmt.Sprintf("%s.s3.amazonaws.com", bucket)
}
func WebsiteEndpoint(bucket string, region string) *S3Website {
domain := WebsiteDomainUrl(region)
return &S3Website{Endpoint: fmt.Sprintf("%s.%s", bucket, domain), Domain: domain}

View File

@ -42,10 +42,14 @@ func TestAccAWSS3Bucket_basic(t *testing.T) {
"aws_s3_bucket.bucket", "hosted_zone_id", HostedZoneIDForRegion("us-west-2")),
resource.TestCheckResourceAttr(
"aws_s3_bucket.bucket", "region", "us-west-2"),
resource.TestCheckResourceAttr(
"aws_s3_bucket.bucket", "website_endpoint", ""),
resource.TestCheckNoResourceAttr(
"aws_s3_bucket.bucket", "website_endpoint"),
resource.TestMatchResourceAttr(
"aws_s3_bucket.bucket", "arn", arnRegexp),
resource.TestCheckResourceAttr(
"aws_s3_bucket.bucket", "bucket", testAccBucketName(rInt)),
resource.TestCheckResourceAttr(
"aws_s3_bucket.bucket", "bucket_domain_name", testAccBucketDomainName(rInt)),
),
},
},
@ -1089,6 +1093,14 @@ func testAccCheckAWSS3BucketLogging(n, b, p string) resource.TestCheckFunc {
// These need a bit of randomness as the name can only be used once globally
// within AWS
func testAccBucketName(randInt int) string {
return fmt.Sprintf("tf-test-bucket-%d", randInt)
}
func testAccBucketDomainName(randInt int) string {
return fmt.Sprintf("tf-test-bucket-%d.s3.amazonaws.com", randInt)
}
func testAccWebsiteEndpoint(randInt int) string {
return fmt.Sprintf("tf-test-bucket-%d.s3-website-us-west-2.amazonaws.com", randInt)
}

View File

@ -102,7 +102,7 @@ func resourceAwsSnsTopicPolicyRead(d *schema.ResourceData, meta interface{}) err
}
func resourceAwsSnsTopicPolicyDelete(d *schema.ResourceData, meta interface{}) error {
accountId, err := getAccountIdFromSnsTopicArn(d.Id())
accountId, err := getAccountIdFromSnsTopicArn(d.Id(), meta.(*AWSClient).partition)
if err != nil {
return err
}
@ -134,9 +134,10 @@ func resourceAwsSnsTopicPolicyDelete(d *schema.ResourceData, meta interface{}) e
return nil
}
func getAccountIdFromSnsTopicArn(arn string) (string, error) {
func getAccountIdFromSnsTopicArn(arn, partition string) (string, error) {
// arn:aws:sns:us-west-2:123456789012:test-new
re := regexp.MustCompile("^arn:aws:sns:[^:]+:([0-9]{12}):.+")
// arn:aws-us-gov:sns:us-west-2:123456789012:test-new
re := regexp.MustCompile(fmt.Sprintf("^arn:%s:sns:[^:]+:([0-9]{12}):.+", partition))
matches := re.FindStringSubmatch(arn)
if len(matches) != 2 {
return "", fmt.Errorf("Unable to get account ID from ARN (%q)", arn)

View File

@ -13,7 +13,7 @@ func TestAccAWSSNSTopicPolicy_basic(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSNSTopicDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSSNSTopicConfig_withPolicy,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSNSTopicExists("aws_sns_topic.test"),

View File

@ -935,11 +935,12 @@ func resourceAwsSpotFleetRequestUpdate(d *schema.ResourceData, meta interface{})
func resourceAwsSpotFleetRequestDelete(d *schema.ResourceData, meta interface{}) error {
// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CancelSpotFleetRequests.html
conn := meta.(*AWSClient).ec2conn
terminateInstances := d.Get("terminate_instances_with_expiration").(bool)
log.Printf("[INFO] Cancelling spot fleet request: %s", d.Id())
resp, err := conn.CancelSpotFleetRequests(&ec2.CancelSpotFleetRequestsInput{
SpotFleetRequestIds: []*string{aws.String(d.Id())},
TerminateInstances: aws.Bool(d.Get("terminate_instances_with_expiration").(bool)),
TerminateInstances: aws.Bool(terminateInstances),
})
if err != nil {
@ -958,6 +959,11 @@ func resourceAwsSpotFleetRequestDelete(d *schema.ResourceData, meta interface{})
return fmt.Errorf("[ERR] Spot Fleet request (%s) was not found to be successfully canceled, dangling resources may exit", d.Id())
}
// Only wait for instance termination if requested
if !terminateInstances {
return nil
}
return resource.Retry(5*time.Minute, func() *resource.RetryError {
resp, err := conn.DescribeSpotFleetInstances(&ec2.DescribeSpotFleetInstancesInput{
SpotFleetRequestId: aws.String(d.Id()),

View File

@ -234,7 +234,7 @@ func validateLambdaFunctionName(v interface{}, k string) (ws []string, errors []
"%q cannot be longer than 140 characters: %q", k, value))
}
// http://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html
pattern := `^(arn:aws:lambda:)?([a-z]{2}-[a-z]+-\d{1}:)?(\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\$LATEST|[a-zA-Z0-9-_]+))?$`
pattern := `^(arn:[\w-]+:lambda:)?([a-z]{2}-[a-z]+-\d{1}:)?(\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\$LATEST|[a-zA-Z0-9-_]+))?$`
if !regexp.MustCompile(pattern).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q doesn't comply with restrictions (%q): %q",
@ -297,7 +297,7 @@ func validateArn(v interface{}, k string) (ws []string, errors []error) {
}
// http://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html
pattern := `^arn:aws:([a-zA-Z0-9\-])+:([a-z]{2}-[a-z]+-\d{1})?:(\d{12})?:(.*)$`
pattern := `^arn:[\w-]+:([a-zA-Z0-9\-])+:([a-z]{2}-[a-z]+-\d{1})?:(\d{12})?:(.*)$`
if !regexp.MustCompile(pattern).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q doesn't look like a valid ARN (%q): %q",

View File

@ -78,6 +78,7 @@ func TestValidateCloudWatchEventRuleName(t *testing.T) {
func TestValidateLambdaFunctionName(t *testing.T) {
validNames := []string{
"arn:aws:lambda:us-west-2:123456789012:function:ThumbNail",
"arn:aws-us-gov:lambda:us-west-2:123456789012:function:ThumbNail",
"FunctionName",
"function-name",
}
@ -203,6 +204,7 @@ func TestValidateArn(t *testing.T) {
"arn:aws:events:us-east-1:319201112229:rule/rule_name", // CloudWatch Rule
"arn:aws:lambda:eu-west-1:319201112229:function:myCustomFunction", // Lambda function
"arn:aws:lambda:eu-west-1:319201112229:function:myCustomFunction:Qualifier", // Lambda func qualifier
"arn:aws-us-gov:s3:::corp_bucket/object.png", // GovCloud ARN
}
for _, v := range validNames {
_, errors := validateArn(v, "arn")

View File

@ -19,6 +19,7 @@ import (
"google.golang.org/api/dns/v1"
"google.golang.org/api/iam/v1"
"google.golang.org/api/pubsub/v1"
"google.golang.org/api/servicemanagement/v1"
"google.golang.org/api/sqladmin/v1beta4"
"google.golang.org/api/storage/v1"
)
@ -38,6 +39,7 @@ type Config struct {
clientStorage *storage.Service
clientSqlAdmin *sqladmin.Service
clientIAM *iam.Service
clientServiceMan *servicemanagement.APIService
}
func (c *Config) loadAndValidate() error {
@ -130,27 +132,34 @@ func (c *Config) loadAndValidate() error {
}
c.clientSqlAdmin.UserAgent = userAgent
log.Printf("[INFO] Instatiating Google Pubsub Client...")
log.Printf("[INFO] Instantiating Google Pubsub Client...")
c.clientPubsub, err = pubsub.New(client)
if err != nil {
return err
}
c.clientPubsub.UserAgent = userAgent
log.Printf("[INFO] Instatiating Google Cloud ResourceManager Client...")
log.Printf("[INFO] Instantiating Google Cloud ResourceManager Client...")
c.clientResourceManager, err = cloudresourcemanager.New(client)
if err != nil {
return err
}
c.clientResourceManager.UserAgent = userAgent
log.Printf("[INFO] Instatiating Google Cloud IAM Client...")
log.Printf("[INFO] Instantiating Google Cloud IAM Client...")
c.clientIAM, err = iam.New(client)
if err != nil {
return err
}
c.clientIAM.UserAgent = userAgent
log.Printf("[INFO] Instantiating Google Cloud Service Management Client...")
c.clientServiceMan, err = servicemanagement.New(client)
if err != nil {
return err
}
c.clientServiceMan.UserAgent = userAgent
return nil
}

View File

@ -4,12 +4,14 @@ import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
)
func TestAccGoogleProject_importBasic(t *testing.T) {
resourceName := "google_project.acceptance"
conf := fmt.Sprintf(testAccGoogleProject_basic, projectId)
projectId := "terraform-" + acctest.RandString(10)
conf := testAccGoogleProject_import(projectId, org, pname)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@ -27,3 +29,12 @@ func TestAccGoogleProject_importBasic(t *testing.T) {
},
})
}
func testAccGoogleProject_import(pid, orgId, projectName string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
project_id = "%s"
org_id = "%s"
name = "%s"
}`, pid, orgId, projectName)
}

View File

@ -96,6 +96,8 @@ func Provider() terraform.ResourceProvider {
"google_sql_database_instance": resourceSqlDatabaseInstance(),
"google_sql_user": resourceSqlUser(),
"google_project": resourceGoogleProject(),
"google_project_iam_policy": resourceGoogleProjectIamPolicy(),
"google_project_services": resourceGoogleProjectServices(),
"google_pubsub_topic": resourcePubsubTopic(),
"google_pubsub_subscription": resourcePubsubSubscription(),
"google_service_account": resourceGoogleServiceAccount(),

View File

@ -13,9 +13,7 @@ import (
)
// resourceGoogleProject returns a *schema.Resource that allows a customer
// to declare a Google Cloud Project resource. //
// Only the 'policy' property of a project may be updated. All other properties
// are computed.
// to declare a Google Cloud Project resource.
//
// This example shows a project with a policy declared in config:
//
@ -25,28 +23,65 @@ import (
// }
func resourceGoogleProject() *schema.Resource {
return &schema.Resource{
SchemaVersion: 1,
Create: resourceGoogleProjectCreate,
Read: resourceGoogleProjectRead,
Update: resourceGoogleProjectUpdate,
Delete: resourceGoogleProjectDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
MigrateState: resourceGoogleProjectMigrateState,
Schema: map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
Type: schema.TypeString,
Optional: true,
Computed: true,
Deprecated: "The id field has unexpected behaviour and probably doesn't do what you expect. See https://www.terraform.io/docs/providers/google/r/google_project.html#id-field for more information. Please use project_id instead; future versions of Terraform will remove the id field.",
},
"policy_data": &schema.Schema{
"project_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
// This suppresses the diff if project_id is not set
if new == "" {
return true
}
return false
},
},
"skip_delete": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Computed: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"org_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"policy_data": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
Deprecated: "Use the 'google_project_iam_policy' resource to define policies for a Google Project",
DiffSuppressFunc: jsonPolicyDiffSuppress,
},
"policy_etag": &schema.Schema{
Type: schema.TypeString,
Computed: true,
Deprecated: "Use the the 'google_project_iam_policy' resource to define policies for a Google Project",
},
"number": &schema.Schema{
Type: schema.TypeString,
Computed: true,
@ -55,20 +90,55 @@ func resourceGoogleProject() *schema.Resource {
}
}
// This resource supports creation, but not in the traditional sense.
// A new Google Cloud Project can not be created. Instead, an existing Project
// is initialized and made available as a Terraform resource.
func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
project, err := getProject(d, config)
if err != nil {
return err
var pid string
var err error
pid = d.Get("project_id").(string)
if pid == "" {
pid, err = getProject(d, config)
if err != nil {
return fmt.Errorf("Error getting project ID: %v", err)
}
if pid == "" {
return fmt.Errorf("'project_id' must be set in the config")
}
}
d.SetId(project)
if err := resourceGoogleProjectRead(d, meta); err != nil {
return err
// we need to check if name and org_id are set, and throw an error if they aren't
// we can't just set these as required on the object, however, as that would break
// all configs that used previous iterations of the resource.
// TODO(paddy): remove this for 0.9 and set these attributes as required.
name, org_id := d.Get("name").(string), d.Get("org_id").(string)
if name == "" {
return fmt.Errorf("`name` must be set in the config if you're creating a project.")
}
if org_id == "" {
return fmt.Errorf("`org_id` must be set in the config if you're creating a project.")
}
log.Printf("[DEBUG]: Creating new project %q", pid)
project := &cloudresourcemanager.Project{
ProjectId: pid,
Name: d.Get("name").(string),
Parent: &cloudresourcemanager.ResourceId{
Id: d.Get("org_id").(string),
Type: "organization",
},
}
op, err := config.clientResourceManager.Projects.Create(project).Do()
if err != nil {
return fmt.Errorf("Error creating project %s (%s): %s.", project.ProjectId, project.Name, err)
}
d.SetId(pid)
// Wait for the operation to complete
waitErr := resourceManagerOperationWait(config, op, "project to create")
if waitErr != nil {
return waitErr
}
// Apply the IAM policy if it is set
@ -76,15 +146,14 @@ func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error
// The policy string is just a marshaled cloudresourcemanager.Policy.
// Unmarshal it to a struct.
var policy cloudresourcemanager.Policy
if err = json.Unmarshal([]byte(pString.(string)), &policy); err != nil {
if err := json.Unmarshal([]byte(pString.(string)), &policy); err != nil {
return err
}
log.Printf("[DEBUG] Got policy from config: %#v", policy.Bindings)
// Retrieve existing IAM policy from project. This will be merged
// with the policy defined here.
// TODO(evanbrown): Add an 'authoritative' flag that allows policy
// in manifest to overwrite existing policy.
p, err := getProjectIamPolicy(project, config)
p, err := getProjectIamPolicy(pid, config)
if err != nil {
return err
}
@ -95,47 +164,98 @@ func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error
// Apply the merged policy
log.Printf("[DEBUG] Setting new policy for project: %#v", p)
_, err = config.clientResourceManager.Projects.SetIamPolicy(project,
_, err = config.clientResourceManager.Projects.SetIamPolicy(pid,
&cloudresourcemanager.SetIamPolicyRequest{Policy: p}).Do()
if err != nil {
return fmt.Errorf("Error applying IAM policy for project %q: %s", project, err)
return fmt.Errorf("Error applying IAM policy for project %q: %s", pid, err)
}
}
return nil
return resourceGoogleProjectRead(d, meta)
}
func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
project, err := getProject(d, config)
pid := d.Id()
// Read the project
p, err := config.clientResourceManager.Projects.Get(pid).Do()
if err != nil {
if v, ok := err.(*googleapi.Error); ok && v.Code == http.StatusNotFound {
return fmt.Errorf("Project %q does not exist.", pid)
}
return fmt.Errorf("Error checking project %q: %s", pid, err)
}
d.Set("project_id", pid)
d.Set("number", strconv.FormatInt(int64(p.ProjectNumber), 10))
d.Set("name", p.Name)
if p.Parent != nil {
d.Set("org_id", p.Parent.Id)
}
// Read the IAM policy
pol, err := getProjectIamPolicy(pid, config)
if err != nil {
return err
}
d.SetId(project)
// Confirm the project exists.
// TODO(evanbrown): Support project creation
p, err := config.clientResourceManager.Projects.Get(project).Do()
polBytes, err := json.Marshal(pol)
if err != nil {
if v, ok := err.(*googleapi.Error); ok && v.Code == http.StatusNotFound {
return fmt.Errorf("Project %q does not exist. The Google provider does not currently support new project creation.", project)
}
return fmt.Errorf("Error checking project %q: %s", project, err)
return err
}
d.Set("number", strconv.FormatInt(int64(p.ProjectNumber), 10))
d.Set("name", p.Name)
d.Set("policy_etag", pol.Etag)
d.Set("policy_data", string(polBytes))
return nil
}
func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
project, err := getProject(d, config)
pid := d.Id()
// Read the project
// we need the project even though refresh has already been called
// because the API doesn't support patch, so we need the actual object
p, err := config.clientResourceManager.Projects.Get(pid).Do()
if err != nil {
return err
if v, ok := err.(*googleapi.Error); ok && v.Code == http.StatusNotFound {
return fmt.Errorf("Project %q does not exist.", pid)
}
return fmt.Errorf("Error checking project %q: %s", pid, err)
}
// Project name has changed
if ok := d.HasChange("name"); ok {
p.Name = d.Get("name").(string)
// Do update on project
p, err = config.clientResourceManager.Projects.Update(p.ProjectId, p).Do()
if err != nil {
return fmt.Errorf("Error updating project %q: %s", p.Name, err)
}
}
return updateProjectIamPolicy(d, config, pid)
}
func resourceGoogleProjectDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
// Only delete projects if skip_delete isn't set
if !d.Get("skip_delete").(bool) {
pid := d.Id()
_, err := config.clientResourceManager.Projects.Delete(pid).Do()
if err != nil {
return fmt.Errorf("Error deleting project %q: %s", pid, err)
}
}
d.SetId("")
return nil
}
func updateProjectIamPolicy(d *schema.ResourceData, config *Config, pid string) error {
// Policy has changed
if ok := d.HasChange("policy_data"); ok {
// The policy string is just a marshaled cloudresourcemanager.Policy.
@ -152,15 +272,13 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error
newPString = "{}"
}
oldPStringf, _ := json.MarshalIndent(oldPString, "", " ")
newPStringf, _ := json.MarshalIndent(newPString, "", " ")
log.Printf("[DEBUG]: Old policy: %v\nNew policy: %v", string(oldPStringf), string(newPStringf))
log.Printf("[DEBUG]: Old policy: %q\nNew policy: %q", oldPString, newPString)
var oldPolicy, newPolicy cloudresourcemanager.Policy
if err = json.Unmarshal([]byte(newPString), &newPolicy); err != nil {
if err := json.Unmarshal([]byte(newPString), &newPolicy); err != nil {
return err
}
if err = json.Unmarshal([]byte(oldPString), &oldPolicy); err != nil {
if err := json.Unmarshal([]byte(oldPString), &oldPolicy); err != nil {
return err
}
@ -199,7 +317,7 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error
// with the policy in the current state
// TODO(evanbrown): Add an 'authoritative' flag that allows policy
// in manifest to overwrite existing policy.
p, err := getProjectIamPolicy(project, config)
p, err := getProjectIamPolicy(pid, config)
if err != nil {
return err
}
@ -218,86 +336,15 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error
}
p.Bindings = rolesToMembersBinding(mergedBindingsMap)
log.Printf("[DEBUG] Setting new policy for project: %#v", p)
dump, _ := json.MarshalIndent(p.Bindings, " ", " ")
log.Printf(string(dump))
_, err = config.clientResourceManager.Projects.SetIamPolicy(project,
log.Printf("[DEBUG] Setting new policy for project: %#v:\n%s", p, string(dump))
_, err = config.clientResourceManager.Projects.SetIamPolicy(pid,
&cloudresourcemanager.SetIamPolicyRequest{Policy: p}).Do()
if err != nil {
return fmt.Errorf("Error applying IAM policy for project %q: %s", project, err)
return fmt.Errorf("Error applying IAM policy for project %q: %s", pid, err)
}
}
return nil
}
func resourceGoogleProjectDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}
// Retrieve the existing IAM Policy for a Project
func getProjectIamPolicy(project string, config *Config) (*cloudresourcemanager.Policy, error) {
p, err := config.clientResourceManager.Projects.GetIamPolicy(project,
&cloudresourcemanager.GetIamPolicyRequest{}).Do()
if err != nil {
return nil, fmt.Errorf("Error retrieving IAM policy for project %q: %s", project, err)
}
return p, nil
}
// Convert a map of roles->members to a list of Binding
func rolesToMembersBinding(m map[string]map[string]bool) []*cloudresourcemanager.Binding {
bindings := make([]*cloudresourcemanager.Binding, 0)
for role, members := range m {
b := cloudresourcemanager.Binding{
Role: role,
Members: make([]string, 0),
}
for m, _ := range members {
b.Members = append(b.Members, m)
}
bindings = append(bindings, &b)
}
return bindings
}
// Map a role to a map of members, allowing easy merging of multiple bindings.
func rolesToMembersMap(bindings []*cloudresourcemanager.Binding) map[string]map[string]bool {
bm := make(map[string]map[string]bool)
// Get each binding
for _, b := range bindings {
// Initialize members map
if _, ok := bm[b.Role]; !ok {
bm[b.Role] = make(map[string]bool)
}
// Get each member (user/principal) for the binding
for _, m := range b.Members {
// Add the member
bm[b.Role][m] = true
}
}
return bm
}
// Merge multiple Bindings such that Bindings with the same Role result in
// a single Binding with combined Members
func mergeBindings(bindings []*cloudresourcemanager.Binding) []*cloudresourcemanager.Binding {
bm := rolesToMembersMap(bindings)
rb := make([]*cloudresourcemanager.Binding, 0)
for role, members := range bm {
var b cloudresourcemanager.Binding
b.Role = role
b.Members = make([]string, 0)
for m, _ := range members {
b.Members = append(b.Members, m)
}
rb = append(rb, &b)
}
return rb
}

View File

@ -0,0 +1,417 @@
package google
import (
"encoding/json"
"fmt"
"log"
"sort"
"github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/cloudresourcemanager/v1"
)
func resourceGoogleProjectIamPolicy() *schema.Resource {
return &schema.Resource{
Create: resourceGoogleProjectIamPolicyCreate,
Read: resourceGoogleProjectIamPolicyRead,
Update: resourceGoogleProjectIamPolicyUpdate,
Delete: resourceGoogleProjectIamPolicyDelete,
Schema: map[string]*schema.Schema{
"project": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"policy_data": &schema.Schema{
Type: schema.TypeString,
Required: true,
DiffSuppressFunc: jsonPolicyDiffSuppress,
},
"authoritative": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"etag": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"restore_policy": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"disable_project": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
},
}
}
func resourceGoogleProjectIamPolicyCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
pid := d.Get("project").(string)
// Get the policy in the template
p, err := getResourceIamPolicy(d)
if err != nil {
return fmt.Errorf("Could not get valid 'policy_data' from resource: %v", err)
}
// An authoritative policy is applied without regard for any existing IAM
// policy.
if v, ok := d.GetOk("authoritative"); ok && v.(bool) {
log.Printf("[DEBUG] Setting authoritative IAM policy for project %q", pid)
err := setProjectIamPolicy(p, config, pid)
if err != nil {
return err
}
} else {
log.Printf("[DEBUG] Setting non-authoritative IAM policy for project %q", pid)
// This is a non-authoritative policy, meaning it should be merged with
// any existing policy
ep, err := getProjectIamPolicy(pid, config)
if err != nil {
return err
}
// First, subtract the policy defined in the template from the
// current policy in the project, and save the result. This will
// allow us to restore the original policy at some point (which
// assumes that Terraform owns any common policy that exists in
// the template and project at create time.
rp := subtractIamPolicy(ep, p)
rps, err := json.Marshal(rp)
if err != nil {
return fmt.Errorf("Error marshaling restorable IAM policy: %v", err)
}
d.Set("restore_policy", string(rps))
// Merge the policies together
mb := mergeBindings(append(p.Bindings, rp.Bindings...))
ep.Bindings = mb
if err = setProjectIamPolicy(ep, config, pid); err != nil {
return fmt.Errorf("Error applying IAM policy to project: %v", err)
}
}
d.SetId(pid)
return resourceGoogleProjectIamPolicyRead(d, meta)
}
func resourceGoogleProjectIamPolicyRead(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG]: Reading google_project_iam_policy")
config := meta.(*Config)
pid := d.Get("project").(string)
p, err := getProjectIamPolicy(pid, config)
if err != nil {
return err
}
var bindings []*cloudresourcemanager.Binding
if v, ok := d.GetOk("restore_policy"); ok {
var restored cloudresourcemanager.Policy
// if there's a restore policy, subtract it from the policy_data
err := json.Unmarshal([]byte(v.(string)), &restored)
if err != nil {
return fmt.Errorf("Error unmarshaling restorable IAM policy: %v", err)
}
subtracted := subtractIamPolicy(p, &restored)
bindings = subtracted.Bindings
} else {
bindings = p.Bindings
}
// we only marshal the bindings, because only the bindings get set in the config
pBytes, err := json.Marshal(&cloudresourcemanager.Policy{Bindings: bindings})
if err != nil {
return fmt.Errorf("Error marshaling IAM policy: %v", err)
}
log.Printf("[DEBUG]: Setting etag=%s", p.Etag)
d.Set("etag", p.Etag)
d.Set("policy_data", string(pBytes))
return nil
}
func resourceGoogleProjectIamPolicyUpdate(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG]: Updating google_project_iam_policy")
config := meta.(*Config)
pid := d.Get("project").(string)
// Get the policy in the template
p, err := getResourceIamPolicy(d)
if err != nil {
return fmt.Errorf("Could not get valid 'policy_data' from resource: %v", err)
}
pBytes, _ := json.Marshal(p)
log.Printf("[DEBUG] Got policy from config: %s", string(pBytes))
// An authoritative policy is applied without regard for any existing IAM
// policy.
if v, ok := d.GetOk("authoritative"); ok && v.(bool) {
log.Printf("[DEBUG] Updating authoritative IAM policy for project %q", pid)
err := setProjectIamPolicy(p, config, pid)
if err != nil {
return fmt.Errorf("Error setting project IAM policy: %v", err)
}
d.Set("restore_policy", "")
} else {
log.Printf("[DEBUG] Updating non-authoritative IAM policy for project %q", pid)
// Get the previous policy from state
pp, err := getPrevResourceIamPolicy(d)
if err != nil {
return fmt.Errorf("Error retrieving previous version of changed project IAM policy: %v", err)
}
ppBytes, _ := json.Marshal(pp)
log.Printf("[DEBUG] Got previous version of changed project IAM policy: %s", string(ppBytes))
// Get the existing IAM policy from the API
ep, err := getProjectIamPolicy(pid, config)
if err != nil {
return fmt.Errorf("Error retrieving IAM policy from project API: %v", err)
}
epBytes, _ := json.Marshal(ep)
log.Printf("[DEBUG] Got existing version of changed IAM policy from project API: %s", string(epBytes))
// Subtract the previous and current policies from the policy retrieved from the API
rp := subtractIamPolicy(ep, pp)
rpBytes, _ := json.Marshal(rp)
log.Printf("[DEBUG] After subtracting the previous policy from the existing policy, remaining policies: %s", string(rpBytes))
rp = subtractIamPolicy(rp, p)
rpBytes, _ = json.Marshal(rp)
log.Printf("[DEBUG] After subtracting the remaining policies from the config policy, remaining policies: %s", string(rpBytes))
rps, err := json.Marshal(rp)
if err != nil {
return fmt.Errorf("Error marhsaling restorable IAM policy: %v", err)
}
d.Set("restore_policy", string(rps))
// Merge the policies together
mb := mergeBindings(append(p.Bindings, rp.Bindings...))
ep.Bindings = mb
if err = setProjectIamPolicy(ep, config, pid); err != nil {
return fmt.Errorf("Error applying IAM policy to project: %v", err)
}
}
return resourceGoogleProjectIamPolicyRead(d, meta)
}
func resourceGoogleProjectIamPolicyDelete(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG]: Deleting google_project_iam_policy")
config := meta.(*Config)
pid := d.Get("project").(string)
// Get the existing IAM policy from the API
ep, err := getProjectIamPolicy(pid, config)
if err != nil {
return fmt.Errorf("Error retrieving IAM policy from project API: %v", err)
}
// Deleting an authoritative policy will leave the project with no policy,
// and unaccessible by anyone without org-level privs. For this reason, the
// "disable_project" property must be set to true, forcing the user to ack
// this outcome
if v, ok := d.GetOk("authoritative"); ok && v.(bool) {
if v, ok := d.GetOk("disable_project"); !ok || !v.(bool) {
return fmt.Errorf("You must set 'disable_project' to true before deleting an authoritative IAM policy")
}
ep.Bindings = make([]*cloudresourcemanager.Binding, 0)
} else {
// A non-authoritative policy should set the policy to the value of "restore_policy" in state
// Get the previous policy from state
rp, err := getRestoreIamPolicy(d)
if err != nil {
return fmt.Errorf("Error retrieving previous version of changed project IAM policy: %v", err)
}
ep.Bindings = rp.Bindings
}
if err = setProjectIamPolicy(ep, config, pid); err != nil {
return fmt.Errorf("Error applying IAM policy to project: %v", err)
}
d.SetId("")
return nil
}
// Subtract all bindings in policy b from policy a, and return the result
func subtractIamPolicy(a, b *cloudresourcemanager.Policy) *cloudresourcemanager.Policy {
am := rolesToMembersMap(a.Bindings)
for _, b := range b.Bindings {
if _, ok := am[b.Role]; ok {
for _, m := range b.Members {
delete(am[b.Role], m)
}
if len(am[b.Role]) == 0 {
delete(am, b.Role)
}
}
}
a.Bindings = rolesToMembersBinding(am)
return a
}
func setProjectIamPolicy(policy *cloudresourcemanager.Policy, config *Config, pid string) error {
// Apply the policy
pbytes, _ := json.Marshal(policy)
log.Printf("[DEBUG] Setting policy %#v for project: %s", string(pbytes), pid)
_, err := config.clientResourceManager.Projects.SetIamPolicy(pid,
&cloudresourcemanager.SetIamPolicyRequest{Policy: policy}).Do()
if err != nil {
return fmt.Errorf("Error applying IAM policy for project %q. Policy is %+s, error is %s", pid, policy, err)
}
return nil
}
// Get a cloudresourcemanager.Policy from a schema.ResourceData
func getResourceIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) {
ps := d.Get("policy_data").(string)
// The policy string is just a marshaled cloudresourcemanager.Policy.
policy := &cloudresourcemanager.Policy{}
if err := json.Unmarshal([]byte(ps), policy); err != nil {
return nil, fmt.Errorf("Could not unmarshal %s:\n: %v", ps, err)
}
return policy, nil
}
// Get the previous cloudresourcemanager.Policy from a schema.ResourceData if the
// resource has changed
func getPrevResourceIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) {
var policy *cloudresourcemanager.Policy = &cloudresourcemanager.Policy{}
if d.HasChange("policy_data") {
v, _ := d.GetChange("policy_data")
if err := json.Unmarshal([]byte(v.(string)), policy); err != nil {
return nil, fmt.Errorf("Could not unmarshal previous policy %s:\n: %v", v, err)
}
}
return policy, nil
}
// Get the restore_policy that can be used to restore a project's IAM policy to its
// state before it was adopted into Terraform
func getRestoreIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) {
if v, ok := d.GetOk("restore_policy"); ok {
policy := &cloudresourcemanager.Policy{}
if err := json.Unmarshal([]byte(v.(string)), policy); err != nil {
return nil, fmt.Errorf("Could not unmarshal previous policy %s:\n: %v", v, err)
}
return policy, nil
}
return nil, fmt.Errorf("Resource does not have a 'restore_policy' attribute defined.")
}
// Retrieve the existing IAM Policy for a Project
func getProjectIamPolicy(project string, config *Config) (*cloudresourcemanager.Policy, error) {
p, err := config.clientResourceManager.Projects.GetIamPolicy(project,
&cloudresourcemanager.GetIamPolicyRequest{}).Do()
if err != nil {
return nil, fmt.Errorf("Error retrieving IAM policy for project %q: %s", project, err)
}
return p, nil
}
// Convert a map of roles->members to a list of Binding
func rolesToMembersBinding(m map[string]map[string]bool) []*cloudresourcemanager.Binding {
bindings := make([]*cloudresourcemanager.Binding, 0)
for role, members := range m {
b := cloudresourcemanager.Binding{
Role: role,
Members: make([]string, 0),
}
for m, _ := range members {
b.Members = append(b.Members, m)
}
bindings = append(bindings, &b)
}
return bindings
}
// Map a role to a map of members, allowing easy merging of multiple bindings.
func rolesToMembersMap(bindings []*cloudresourcemanager.Binding) map[string]map[string]bool {
bm := make(map[string]map[string]bool)
// Get each binding
for _, b := range bindings {
// Initialize members map
if _, ok := bm[b.Role]; !ok {
bm[b.Role] = make(map[string]bool)
}
// Get each member (user/principal) for the binding
for _, m := range b.Members {
// Add the member
bm[b.Role][m] = true
}
}
return bm
}
// Merge multiple Bindings such that Bindings with the same Role result in
// a single Binding with combined Members
func mergeBindings(bindings []*cloudresourcemanager.Binding) []*cloudresourcemanager.Binding {
bm := rolesToMembersMap(bindings)
rb := make([]*cloudresourcemanager.Binding, 0)
for role, members := range bm {
var b cloudresourcemanager.Binding
b.Role = role
b.Members = make([]string, 0)
for m, _ := range members {
b.Members = append(b.Members, m)
}
rb = append(rb, &b)
}
return rb
}
func jsonPolicyDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
var oldPolicy, newPolicy cloudresourcemanager.Policy
if err := json.Unmarshal([]byte(old), &oldPolicy); err != nil {
log.Printf("[ERROR] Could not unmarshal old policy %s: %v", old, err)
return false
}
if err := json.Unmarshal([]byte(new), &newPolicy); err != nil {
log.Printf("[ERROR] Could not unmarshal new policy %s: %v", new, err)
return false
}
if newPolicy.Etag != oldPolicy.Etag {
return false
}
if newPolicy.Version != oldPolicy.Version {
return false
}
if len(newPolicy.Bindings) != len(oldPolicy.Bindings) {
return false
}
sort.Sort(sortableBindings(newPolicy.Bindings))
sort.Sort(sortableBindings(oldPolicy.Bindings))
for pos, newBinding := range newPolicy.Bindings {
oldBinding := oldPolicy.Bindings[pos]
if oldBinding.Role != newBinding.Role {
return false
}
if len(oldBinding.Members) != len(newBinding.Members) {
return false
}
sort.Strings(oldBinding.Members)
sort.Strings(newBinding.Members)
for i, newMember := range newBinding.Members {
oldMember := oldBinding.Members[i]
if newMember != oldMember {
return false
}
}
}
return true
}
type sortableBindings []*cloudresourcemanager.Binding
func (b sortableBindings) Len() int {
return len(b)
}
func (b sortableBindings) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}
func (b sortableBindings) Less(i, j int) bool {
return b[i].Role < b[j].Role
}

View File

@ -0,0 +1,626 @@
package google
import (
"encoding/json"
"fmt"
"reflect"
"sort"
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"google.golang.org/api/cloudresourcemanager/v1"
)
func TestSubtractIamPolicy(t *testing.T) {
table := []struct {
a *cloudresourcemanager.Policy
b *cloudresourcemanager.Policy
expect cloudresourcemanager.Policy
}{
{
a: &cloudresourcemanager.Policy{
Bindings: []*cloudresourcemanager.Binding{
{
Role: "a",
Members: []string{
"1",
"2",
},
},
{
Role: "b",
Members: []string{
"1",
"2",
},
},
},
},
b: &cloudresourcemanager.Policy{
Bindings: []*cloudresourcemanager.Binding{
{
Role: "a",
Members: []string{
"3",
"4",
},
},
{
Role: "b",
Members: []string{
"1",
"2",
},
},
},
},
expect: cloudresourcemanager.Policy{
Bindings: []*cloudresourcemanager.Binding{
{
Role: "a",
Members: []string{
"1",
"2",
},
},
},
},
},
{
a: &cloudresourcemanager.Policy{
Bindings: []*cloudresourcemanager.Binding{
{
Role: "a",
Members: []string{
"1",
"2",
},
},
{
Role: "b",
Members: []string{
"1",
"2",
},
},
},
},
b: &cloudresourcemanager.Policy{
Bindings: []*cloudresourcemanager.Binding{
{
Role: "a",
Members: []string{
"1",
"2",
},
},
{
Role: "b",
Members: []string{
"1",
"2",
},
},
},
},
expect: cloudresourcemanager.Policy{
Bindings: []*cloudresourcemanager.Binding{},
},
},
{
a: &cloudresourcemanager.Policy{
Bindings: []*cloudresourcemanager.Binding{
{
Role: "a",
Members: []string{
"1",
"2",
"3",
},
},
{
Role: "b",
Members: []string{
"1",
"2",
"3",
},
},
},
},
b: &cloudresourcemanager.Policy{
Bindings: []*cloudresourcemanager.Binding{
{
Role: "a",
Members: []string{
"1",
"3",
},
},
{
Role: "b",
Members: []string{
"1",
"2",
"3",
},
},
},
},
expect: cloudresourcemanager.Policy{
Bindings: []*cloudresourcemanager.Binding{
{
Role: "a",
Members: []string{
"2",
},
},
},
},
},
{
a: &cloudresourcemanager.Policy{
Bindings: []*cloudresourcemanager.Binding{
{
Role: "a",
Members: []string{
"1",
"2",
"3",
},
},
{
Role: "b",
Members: []string{
"1",
"2",
"3",
},
},
},
},
b: &cloudresourcemanager.Policy{
Bindings: []*cloudresourcemanager.Binding{
{
Role: "a",
Members: []string{
"1",
"2",
"3",
},
},
{
Role: "b",
Members: []string{
"1",
"2",
"3",
},
},
},
},
expect: cloudresourcemanager.Policy{
Bindings: []*cloudresourcemanager.Binding{},
},
},
}
for _, test := range table {
c := subtractIamPolicy(test.a, test.b)
sort.Sort(sortableBindings(c.Bindings))
for i, _ := range c.Bindings {
sort.Strings(c.Bindings[i].Members)
}
if !reflect.DeepEqual(derefBindings(c.Bindings), derefBindings(test.expect.Bindings)) {
t.Errorf("\ngot %+v\nexpected %+v", derefBindings(c.Bindings), derefBindings(test.expect.Bindings))
}
}
}
// Test that an IAM policy can be applied to a project
func TestAccGoogleProjectIamPolicy_basic(t *testing.T) {
pid := "terraform-" + acctest.RandString(10)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
// Create a new project
resource.TestStep{
Config: testAccGoogleProject_create(pid, pname, org),
Check: resource.ComposeTestCheckFunc(
testAccGoogleProjectExistingPolicy(pid),
),
},
// Apply an IAM policy from a data source. The application
// merges policies, so we validate the expected state.
resource.TestStep{
Config: testAccGoogleProjectAssociatePolicyBasic(pid, pname, org),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleProjectIamPolicyIsMerged("google_project_iam_policy.acceptance", "data.google_iam_policy.admin", pid),
),
},
// Finally, remove the custom IAM policy from config and apply, then
// confirm that the project is in its original state.
resource.TestStep{
Config: testAccGoogleProject_create(pid, pname, org),
Check: resource.ComposeTestCheckFunc(
testAccGoogleProjectExistingPolicy(pid),
),
},
},
})
}
func testAccCheckGoogleProjectIamPolicyIsMerged(projectRes, policyRes, pid string) resource.TestCheckFunc {
return func(s *terraform.State) error {
// Get the project resource
project, ok := s.RootModule().Resources[projectRes]
if !ok {
return fmt.Errorf("Not found: %s", projectRes)
}
// The project ID should match the config's project ID
if project.Primary.ID != pid {
return fmt.Errorf("Expected project %q to match ID %q in state", pid, project.Primary.ID)
}
var projectP, policyP cloudresourcemanager.Policy
// The project should have a policy
ps, ok := project.Primary.Attributes["policy_data"]
if !ok {
return fmt.Errorf("Project resource %q did not have a 'policy_data' attribute. Attributes were %#v", project.Primary.Attributes["id"], project.Primary.Attributes)
}
if err := json.Unmarshal([]byte(ps), &projectP); err != nil {
return fmt.Errorf("Could not unmarshal %s:\n: %v", ps, err)
}
// The data policy resource should have a policy
policy, ok := s.RootModule().Resources[policyRes]
if !ok {
return fmt.Errorf("Not found: %s", policyRes)
}
ps, ok = policy.Primary.Attributes["policy_data"]
if !ok {
return fmt.Errorf("Data policy resource %q did not have a 'policy_data' attribute. Attributes were %#v", policy.Primary.Attributes["id"], project.Primary.Attributes)
}
if err := json.Unmarshal([]byte(ps), &policyP); err != nil {
return err
}
// The bindings in both policies should be identical
sort.Sort(sortableBindings(projectP.Bindings))
sort.Sort(sortableBindings(policyP.Bindings))
if !reflect.DeepEqual(derefBindings(projectP.Bindings), derefBindings(policyP.Bindings)) {
return fmt.Errorf("Project and data source policies do not match: project policy is %+v, data resource policy is %+v", derefBindings(projectP.Bindings), derefBindings(policyP.Bindings))
}
// Merge the project policy in Terraform state with the policy the project had before the config was applied
expected := make([]*cloudresourcemanager.Binding, 0)
expected = append(expected, originalPolicy.Bindings...)
expected = append(expected, projectP.Bindings...)
expectedM := mergeBindings(expected)
// Retrieve the actual policy from the project
c := testAccProvider.Meta().(*Config)
actual, err := getProjectIamPolicy(pid, c)
if err != nil {
return fmt.Errorf("Failed to retrieve IAM Policy for project %q: %s", pid, err)
}
actualM := mergeBindings(actual.Bindings)
sort.Sort(sortableBindings(actualM))
sort.Sort(sortableBindings(expectedM))
// The bindings should match, indicating the policy was successfully applied and merged
if !reflect.DeepEqual(derefBindings(actualM), derefBindings(expectedM)) {
return fmt.Errorf("Actual and expected project policies do not match: actual policy is %+v, expected policy is %+v", derefBindings(actualM), derefBindings(expectedM))
}
return nil
}
}
func TestIamRolesToMembersBinding(t *testing.T) {
table := []struct {
expect []*cloudresourcemanager.Binding
input map[string]map[string]bool
}{
{
expect: []*cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-1",
"member-2",
},
},
},
input: map[string]map[string]bool{
"role-1": map[string]bool{
"member-1": true,
"member-2": true,
},
},
},
{
expect: []*cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-1",
"member-2",
},
},
},
input: map[string]map[string]bool{
"role-1": map[string]bool{
"member-1": true,
"member-2": true,
},
},
},
{
expect: []*cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{},
},
},
input: map[string]map[string]bool{
"role-1": map[string]bool{},
},
},
}
for _, test := range table {
got := rolesToMembersBinding(test.input)
sort.Sort(sortableBindings(got))
for i, _ := range got {
sort.Strings(got[i].Members)
}
if !reflect.DeepEqual(derefBindings(got), derefBindings(test.expect)) {
t.Errorf("got %+v, expected %+v", derefBindings(got), derefBindings(test.expect))
}
}
}
func TestIamRolesToMembersMap(t *testing.T) {
table := []struct {
input []*cloudresourcemanager.Binding
expect map[string]map[string]bool
}{
{
input: []*cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-1",
"member-2",
},
},
},
expect: map[string]map[string]bool{
"role-1": map[string]bool{
"member-1": true,
"member-2": true,
},
},
},
{
input: []*cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-1",
"member-2",
"member-1",
"member-2",
},
},
},
expect: map[string]map[string]bool{
"role-1": map[string]bool{
"member-1": true,
"member-2": true,
},
},
},
{
input: []*cloudresourcemanager.Binding{
{
Role: "role-1",
},
},
expect: map[string]map[string]bool{
"role-1": map[string]bool{},
},
},
}
for _, test := range table {
got := rolesToMembersMap(test.input)
if !reflect.DeepEqual(got, test.expect) {
t.Errorf("got %+v, expected %+v", got, test.expect)
}
}
}
func TestIamMergeBindings(t *testing.T) {
table := []struct {
input []*cloudresourcemanager.Binding
expect []cloudresourcemanager.Binding
}{
{
input: []*cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-1",
"member-2",
},
},
{
Role: "role-1",
Members: []string{
"member-3",
},
},
},
expect: []cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-1",
"member-2",
"member-3",
},
},
},
},
{
input: []*cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-3",
"member-4",
},
},
{
Role: "role-1",
Members: []string{
"member-2",
"member-1",
},
},
{
Role: "role-2",
Members: []string{
"member-1",
},
},
{
Role: "role-1",
Members: []string{
"member-5",
},
},
{
Role: "role-3",
Members: []string{
"member-1",
},
},
{
Role: "role-2",
Members: []string{
"member-2",
},
},
},
expect: []cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-1",
"member-2",
"member-3",
"member-4",
"member-5",
},
},
{
Role: "role-2",
Members: []string{
"member-1",
"member-2",
},
},
{
Role: "role-3",
Members: []string{
"member-1",
},
},
},
},
}
for _, test := range table {
got := mergeBindings(test.input)
sort.Sort(sortableBindings(got))
for i, _ := range got {
sort.Strings(got[i].Members)
}
if !reflect.DeepEqual(derefBindings(got), test.expect) {
t.Errorf("\ngot %+v\nexpected %+v", derefBindings(got), test.expect)
}
}
}
func derefBindings(b []*cloudresourcemanager.Binding) []cloudresourcemanager.Binding {
db := make([]cloudresourcemanager.Binding, len(b))
for i, v := range b {
db[i] = *v
sort.Strings(db[i].Members)
}
return db
}
// Confirm that a project has an IAM policy with at least 1 binding
func testAccGoogleProjectExistingPolicy(pid string) resource.TestCheckFunc {
return func(s *terraform.State) error {
c := testAccProvider.Meta().(*Config)
var err error
originalPolicy, err = getProjectIamPolicy(pid, c)
if err != nil {
return fmt.Errorf("Failed to retrieve IAM Policy for project %q: %s", pid, err)
}
if len(originalPolicy.Bindings) == 0 {
return fmt.Errorf("Refuse to run test against project with zero IAM Bindings. This is likely an error in the test code that is not properly identifying the IAM policy of a project.")
}
return nil
}
}
func testAccGoogleProjectAssociatePolicyBasic(pid, name, org string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
project_id = "%s"
name = "%s"
org_id = "%s"
}
resource "google_project_iam_policy" "acceptance" {
project = "${google_project.acceptance.id}"
policy_data = "${data.google_iam_policy.admin.policy_data}"
}
data "google_iam_policy" "admin" {
binding {
role = "roles/storage.objectViewer"
members = [
"user:evanbrown@google.com",
]
}
binding {
role = "roles/compute.instanceAdmin"
members = [
"user:evanbrown@google.com",
"user:evandbrown@gmail.com",
]
}
}
`, pid, name, org)
}
func testAccGoogleProject_create(pid, name, org string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
project_id = "%s"
name = "%s"
org_id = "%s"
}`, pid, name, org)
}

View File

@ -0,0 +1,47 @@
package google
import (
"fmt"
"log"
"github.com/hashicorp/terraform/terraform"
)
func resourceGoogleProjectMigrateState(v int, s *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
if s.Empty() {
log.Println("[DEBUG] Empty InstanceState; nothing to migrate.")
return s, nil
}
switch v {
case 0:
log.Println("[INFO] Found Google Project State v0; migrating to v1")
s, err := migrateGoogleProjectStateV0toV1(s, meta.(*Config))
if err != nil {
return s, err
}
return s, nil
default:
return s, fmt.Errorf("Unexpected schema version: %d", v)
}
}
// This migration adjusts google_project resources to include several additional attributes
// required to support project creation/deletion that was added in V1.
func migrateGoogleProjectStateV0toV1(s *terraform.InstanceState, config *Config) (*terraform.InstanceState, error) {
log.Printf("[DEBUG] Attributes before migration: %#v", s.Attributes)
s.Attributes["skip_delete"] = "true"
s.Attributes["project_id"] = s.ID
if s.Attributes["policy_data"] != "" {
p, err := getProjectIamPolicy(s.ID, config)
if err != nil {
return s, fmt.Errorf("Could not retrieve project's IAM policy while attempting to migrate state from V0 to V1: %v", err)
}
s.Attributes["policy_etag"] = p.Etag
}
log.Printf("[DEBUG] Attributes after migration: %#v", s.Attributes)
return s, nil
}

View File

@ -0,0 +1,70 @@
package google
import (
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestGoogleProjectMigrateState(t *testing.T) {
cases := map[string]struct {
StateVersion int
Attributes map[string]string
Expected map[string]string
Meta interface{}
}{
"deprecate policy_data and support creation/deletion": {
StateVersion: 0,
Attributes: map[string]string{},
Expected: map[string]string{
"project_id": "test-project",
"skip_delete": "true",
},
Meta: &Config{},
},
}
for tn, tc := range cases {
is := &terraform.InstanceState{
ID: "test-project",
Attributes: tc.Attributes,
}
is, err := resourceGoogleProjectMigrateState(
tc.StateVersion, is, tc.Meta)
if err != nil {
t.Fatalf("bad: %s, err: %#v", tn, err)
}
for k, v := range tc.Expected {
if is.Attributes[k] != v {
t.Fatalf(
"bad: %s\n\n expected: %#v -> %#v\n got: %#v -> %#v\n in: %#v",
tn, k, v, k, is.Attributes[k], is.Attributes)
}
}
}
}
func TestGoogleProjectMigrateState_empty(t *testing.T) {
var is *terraform.InstanceState
var meta *Config
// should handle nil
is, err := resourceGoogleProjectMigrateState(0, is, meta)
if err != nil {
t.Fatalf("err: %#v", err)
}
if is != nil {
t.Fatalf("expected nil instancestate, got: %#v", is)
}
// should handle non-nil but empty
is = &terraform.InstanceState{}
is, err = resourceGoogleProjectMigrateState(0, is, meta)
if err != nil {
t.Fatalf("err: %#v", err)
}
}

View File

@ -0,0 +1,214 @@
package google
import (
"fmt"
"log"
"github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/servicemanagement/v1"
)
func resourceGoogleProjectServices() *schema.Resource {
return &schema.Resource{
Create: resourceGoogleProjectServicesCreate,
Read: resourceGoogleProjectServicesRead,
Update: resourceGoogleProjectServicesUpdate,
Delete: resourceGoogleProjectServicesDelete,
Schema: map[string]*schema.Schema{
"project": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"services": {
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
},
}
}
func resourceGoogleProjectServicesCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
pid := d.Get("project").(string)
// Get services from config
cfgServices := getConfigServices(d)
// Get services from API
apiServices, err := getApiServices(pid, config)
if err != nil {
return fmt.Errorf("Error creating services: %v", err)
}
// This call disables any APIs that aren't defined in cfgServices,
// and enables all of those that are
err = reconcileServices(cfgServices, apiServices, config, pid)
if err != nil {
return fmt.Errorf("Error creating services: %v", err)
}
d.SetId(pid)
return resourceGoogleProjectServicesRead(d, meta)
}
func resourceGoogleProjectServicesRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
services, err := getApiServices(d.Id(), config)
if err != nil {
return err
}
d.Set("services", services)
return nil
}
func resourceGoogleProjectServicesUpdate(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG]: Updating google_project_services")
config := meta.(*Config)
pid := d.Get("project").(string)
// Get services from config
cfgServices := getConfigServices(d)
// Get services from API
apiServices, err := getApiServices(pid, config)
if err != nil {
return fmt.Errorf("Error updating services: %v", err)
}
// This call disables any APIs that aren't defined in cfgServices,
// and enables all of those that are
err = reconcileServices(cfgServices, apiServices, config, pid)
if err != nil {
return fmt.Errorf("Error updating services: %v", err)
}
return resourceGoogleProjectServicesRead(d, meta)
}
func resourceGoogleProjectServicesDelete(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG]: Deleting google_project_services")
config := meta.(*Config)
services := resourceServices(d)
for _, s := range services {
disableService(s, d.Id(), config)
}
d.SetId("")
return nil
}
// This function ensures that the services enabled for a project exactly match that
// in a config by disabling any services that are returned by the API but not present
// in the config
func reconcileServices(cfgServices, apiServices []string, config *Config, pid string) error {
// Helper to convert slice to map
m := func(vals []string) map[string]struct{} {
sm := make(map[string]struct{})
for _, s := range vals {
sm[s] = struct{}{}
}
return sm
}
cfgMap := m(cfgServices)
apiMap := m(apiServices)
for k, _ := range apiMap {
if _, ok := cfgMap[k]; !ok {
// The service in the API is not in the config; disable it.
err := disableService(k, pid, config)
if err != nil {
return err
}
} else {
// The service exists in the config and the API, so we don't need
// to re-enable it
delete(cfgMap, k)
}
}
for k, _ := range cfgMap {
err := enableService(k, pid, config)
if err != nil {
return err
}
}
return nil
}
// Retrieve services defined in a config
func getConfigServices(d *schema.ResourceData) (services []string) {
if v, ok := d.GetOk("services"); ok {
for _, svc := range v.(*schema.Set).List() {
services = append(services, svc.(string))
}
}
return
}
// Retrieve a project's services from the API
func getApiServices(pid string, config *Config) ([]string, error) {
apiServices := make([]string, 0)
// Get services from the API
svcResp, err := config.clientServiceMan.Services.List().ConsumerId("project:" + pid).Do()
if err != nil {
return apiServices, err
}
for _, v := range svcResp.Services {
apiServices = append(apiServices, v.ServiceName)
}
return apiServices, nil
}
func enableService(s, pid string, config *Config) error {
esr := newEnableServiceRequest(pid)
sop, err := config.clientServiceMan.Services.Enable(s, esr).Do()
if err != nil {
return fmt.Errorf("Error enabling service %q for project %q: %v", s, pid, err)
}
// Wait for the operation to complete
waitErr := serviceManagementOperationWait(config, sop, "api to enable")
if waitErr != nil {
return waitErr
}
return nil
}
func disableService(s, pid string, config *Config) error {
dsr := newDisableServiceRequest(pid)
sop, err := config.clientServiceMan.Services.Disable(s, dsr).Do()
if err != nil {
return fmt.Errorf("Error disabling service %q for project %q: %v", s, pid, err)
}
// Wait for the operation to complete
waitErr := serviceManagementOperationWait(config, sop, "api to disable")
if waitErr != nil {
return waitErr
}
return nil
}
func newEnableServiceRequest(pid string) *servicemanagement.EnableServiceRequest {
return &servicemanagement.EnableServiceRequest{ConsumerId: "project:" + pid}
}
func newDisableServiceRequest(pid string) *servicemanagement.DisableServiceRequest {
return &servicemanagement.DisableServiceRequest{ConsumerId: "project:" + pid}
}
func resourceServices(d *schema.ResourceData) []string {
// Calculate the tags
var services []string
if s := d.Get("services"); s != nil {
ss := s.(*schema.Set)
services = make([]string, ss.Len())
for i, v := range ss.List() {
services[i] = v.(string)
}
}
return services
}

View File

@ -0,0 +1,178 @@
package google
import (
"bytes"
"fmt"
"log"
"reflect"
"sort"
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"google.golang.org/api/servicemanagement/v1"
)
// Test that services can be enabled and disabled on a project
func TestAccGoogleProjectServices_basic(t *testing.T) {
pid := "terraform-" + acctest.RandString(10)
services1 := []string{"iam.googleapis.com", "cloudresourcemanager.googleapis.com"}
services2 := []string{"cloudresourcemanager.googleapis.com"}
oobService := "iam.googleapis.com"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
// Create a new project with some services
resource.TestStep{
Config: testAccGoogleProjectAssociateServicesBasic(services1, pid, pname, org),
Check: resource.ComposeTestCheckFunc(
testProjectServicesMatch(services1, pid),
),
},
// Update services to remove one
resource.TestStep{
Config: testAccGoogleProjectAssociateServicesBasic(services2, pid, pname, org),
Check: resource.ComposeTestCheckFunc(
testProjectServicesMatch(services2, pid),
),
},
// Add a service out-of-band and ensure it is removed
resource.TestStep{
PreConfig: func() {
config := testAccProvider.Meta().(*Config)
enableService(oobService, pid, config)
},
Config: testAccGoogleProjectAssociateServicesBasic(services2, pid, pname, org),
Check: resource.ComposeTestCheckFunc(
testProjectServicesMatch(services2, pid),
),
},
},
})
}
// Test that services are authoritative when a project has existing
// sevices not represented in config
func TestAccGoogleProjectServices_authoritative(t *testing.T) {
pid := "terraform-" + acctest.RandString(10)
services := []string{"cloudresourcemanager.googleapis.com"}
oobService := "iam.googleapis.com"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
// Create a new project with no services
resource.TestStep{
Config: testAccGoogleProject_create(pid, pname, org),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleProjectExists("google_project.acceptance", pid),
),
},
// Add a service out-of-band, then apply a config that creates a service.
// It should remove the out-of-band service.
resource.TestStep{
PreConfig: func() {
config := testAccProvider.Meta().(*Config)
enableService(oobService, pid, config)
},
Config: testAccGoogleProjectAssociateServicesBasic(services, pid, pname, org),
Check: resource.ComposeTestCheckFunc(
testProjectServicesMatch(services, pid),
),
},
},
})
}
// Test that services are authoritative when a project has existing
// sevices, some which are represented in the config and others
// that are not
func TestAccGoogleProjectServices_authoritative2(t *testing.T) {
pid := "terraform-" + acctest.RandString(10)
oobServices := []string{"iam.googleapis.com", "cloudresourcemanager.googleapis.com"}
services := []string{"iam.googleapis.com"}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
// Create a new project with no services
resource.TestStep{
Config: testAccGoogleProject_create(pid, pname, org),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleProjectExists("google_project.acceptance", pid),
),
},
// Add a service out-of-band, then apply a config that creates a service.
// It should remove the out-of-band service.
resource.TestStep{
PreConfig: func() {
config := testAccProvider.Meta().(*Config)
for _, s := range oobServices {
enableService(s, pid, config)
}
},
Config: testAccGoogleProjectAssociateServicesBasic(services, pid, pname, org),
Check: resource.ComposeTestCheckFunc(
testProjectServicesMatch(services, pid),
),
},
},
})
}
func testAccGoogleProjectAssociateServicesBasic(services []string, pid, name, org string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
project_id = "%s"
name = "%s"
org_id = "%s"
}
resource "google_project_services" "acceptance" {
project = "${google_project.acceptance.project_id}"
services = [%s]
}
`, pid, name, org, testStringsToString(services))
}
func testProjectServicesMatch(services []string, pid string) resource.TestCheckFunc {
return func(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
apiServices, err := getApiServices(pid, config)
if err != nil {
return fmt.Errorf("Error listing services for project %q: %v", pid, err)
}
sort.Strings(services)
sort.Strings(apiServices)
if !reflect.DeepEqual(services, apiServices) {
return fmt.Errorf("Services in config (%v) do not exactly match services returned by API (%v)", services, apiServices)
}
return nil
}
}
func testStringsToString(s []string) string {
var b bytes.Buffer
for i, v := range s {
b.WriteString(fmt.Sprintf("\"%s\"", v))
if i < len(s)-1 {
b.WriteString(",")
}
}
r := b.String()
log.Printf("[DEBUG]: Converted list of strings to %s", r)
return b.String()
}
func testManagedServicesToString(svcs []*servicemanagement.ManagedService) string {
var b bytes.Buffer
for _, s := range svcs {
b.WriteString(s.ServiceName)
}
return b.String()
}

View File

@ -1,24 +1,23 @@
package google
import (
"encoding/json"
"fmt"
"os"
"reflect"
"sort"
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"google.golang.org/api/cloudresourcemanager/v1"
)
var (
projectId = multiEnvSearch([]string{
"GOOGLE_PROJECT",
"GCLOUD_PROJECT",
"CLOUDSDK_CORE_PROJECT",
org = multiEnvSearch([]string{
"GOOGLE_ORG",
})
pname = "Terraform Acceptance Tests"
originalPolicy *cloudresourcemanager.Policy
)
func multiEnvSearch(ks []string) string {
@ -30,77 +29,26 @@ func multiEnvSearch(ks []string) string {
return ""
}
// Test that a Project resource can be created and destroyed
func TestAccGoogleProject_associate(t *testing.T) {
// Test that a Project resource can be created and an IAM policy
// associated
func TestAccGoogleProject_create(t *testing.T) {
pid := "terraform-" + acctest.RandString(10)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
// This step imports an existing project
resource.TestStep{
Config: fmt.Sprintf(testAccGoogleProject_basic, projectId),
Config: testAccGoogleProject_create(pid, pname, org),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleProjectExists("google_project.acceptance"),
testAccCheckGoogleProjectExists("google_project.acceptance", pid),
),
},
},
})
}
// Test that a Project resource can be created, an IAM Policy
// associated with it, and then destroyed
func TestAccGoogleProject_iamPolicy1(t *testing.T) {
var policy *cloudresourcemanager.Policy
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckGoogleProjectDestroy,
Steps: []resource.TestStep{
// First step inventories the project's existing IAM policy
resource.TestStep{
Config: fmt.Sprintf(testAccGoogleProject_basic, projectId),
Check: resource.ComposeTestCheckFunc(
testAccGoogleProjectExistingPolicy(policy),
),
},
// Second step applies an IAM policy from a data source. The application
// merges policies, so we validate the expected state.
resource.TestStep{
Config: fmt.Sprintf(testAccGoogleProject_policy1, projectId),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleProjectExists("google_project.acceptance"),
testAccCheckGoogleProjectIamPolicyIsMerged("google_project.acceptance", "data.google_iam_policy.admin", policy),
),
},
// Finally, remove the custom IAM policy from config and apply, then
// confirm that the project is in its original state.
resource.TestStep{
Config: fmt.Sprintf(testAccGoogleProject_basic, projectId),
},
},
})
}
func testAccCheckGoogleProjectDestroy(s *terraform.State) error {
return nil
}
// Retrieve the existing policy (if any) for a GCP Project
func testAccGoogleProjectExistingPolicy(p *cloudresourcemanager.Policy) resource.TestCheckFunc {
return func(s *terraform.State) error {
c := testAccProvider.Meta().(*Config)
var err error
p, err = getProjectIamPolicy(projectId, c)
if err != nil {
return fmt.Errorf("Failed to retrieve IAM Policy for project %q: %s", projectId, err)
}
if len(p.Bindings) == 0 {
return fmt.Errorf("Refuse to run test against project with zero IAM Bindings. This is likely an error in the test code that is not properly identifying the IAM policy of a project.")
}
return nil
}
}
func testAccCheckGoogleProjectExists(r string) resource.TestCheckFunc {
func testAccCheckGoogleProjectExists(r, pid string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[r]
if !ok {
@ -111,349 +59,29 @@ func testAccCheckGoogleProjectExists(r string) resource.TestCheckFunc {
return fmt.Errorf("No ID is set")
}
if rs.Primary.ID != projectId {
return fmt.Errorf("Expected project %q to match ID %q in state", projectId, rs.Primary.ID)
if rs.Primary.ID != pid {
return fmt.Errorf("Expected project %q to match ID %q in state", pid, rs.Primary.ID)
}
return nil
}
}
func testAccCheckGoogleProjectIamPolicyIsMerged(projectRes, policyRes string, original *cloudresourcemanager.Policy) resource.TestCheckFunc {
return func(s *terraform.State) error {
// Get the project resource
project, ok := s.RootModule().Resources[projectRes]
if !ok {
return fmt.Errorf("Not found: %s", projectRes)
}
// The project ID should match the config's project ID
if project.Primary.ID != projectId {
return fmt.Errorf("Expected project %q to match ID %q in state", projectId, project.Primary.ID)
}
var projectP, policyP cloudresourcemanager.Policy
// The project should have a policy
ps, ok := project.Primary.Attributes["policy_data"]
if !ok {
return fmt.Errorf("Project resource %q did not have a 'policy_data' attribute. Attributes were %#v", project.Primary.Attributes["id"], project.Primary.Attributes)
}
if err := json.Unmarshal([]byte(ps), &projectP); err != nil {
return err
}
// The data policy resource should have a policy
policy, ok := s.RootModule().Resources[policyRes]
if !ok {
return fmt.Errorf("Not found: %s", policyRes)
}
ps, ok = policy.Primary.Attributes["policy_data"]
if !ok {
return fmt.Errorf("Data policy resource %q did not have a 'policy_data' attribute. Attributes were %#v", policy.Primary.Attributes["id"], project.Primary.Attributes)
}
if err := json.Unmarshal([]byte(ps), &policyP); err != nil {
return err
}
// The bindings in both policies should be identical
if !reflect.DeepEqual(derefBindings(projectP.Bindings), derefBindings(policyP.Bindings)) {
return fmt.Errorf("Project and data source policies do not match: project policy is %+v, data resource policy is %+v", derefBindings(projectP.Bindings), derefBindings(policyP.Bindings))
}
// Merge the project policy in Terrafomr state with the policy the project had before the config was applied
expected := make([]*cloudresourcemanager.Binding, 0)
expected = append(expected, original.Bindings...)
expected = append(expected, projectP.Bindings...)
expectedM := mergeBindings(expected)
// Retrieve the actual policy from the project
c := testAccProvider.Meta().(*Config)
actual, err := getProjectIamPolicy(projectId, c)
if err != nil {
return fmt.Errorf("Failed to retrieve IAM Policy for project %q: %s", projectId, err)
}
actualM := mergeBindings(actual.Bindings)
// The bindings should match, indicating the policy was successfully applied and merged
if !reflect.DeepEqual(derefBindings(actualM), derefBindings(expectedM)) {
return fmt.Errorf("Actual and expected project policies do not match: actual policy is %+v, expected policy is %+v", derefBindings(actualM), derefBindings(expectedM))
}
return nil
}
}
func TestIamRolesToMembersBinding(t *testing.T) {
table := []struct {
expect []*cloudresourcemanager.Binding
input map[string]map[string]bool
}{
{
expect: []*cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-1",
"member-2",
},
},
},
input: map[string]map[string]bool{
"role-1": map[string]bool{
"member-1": true,
"member-2": true,
},
},
},
{
expect: []*cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-1",
"member-2",
},
},
},
input: map[string]map[string]bool{
"role-1": map[string]bool{
"member-1": true,
"member-2": true,
},
},
},
{
expect: []*cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{},
},
},
input: map[string]map[string]bool{
"role-1": map[string]bool{},
},
},
}
for _, test := range table {
got := rolesToMembersBinding(test.input)
sort.Sort(Binding(got))
for i, _ := range got {
sort.Strings(got[i].Members)
}
if !reflect.DeepEqual(derefBindings(got), derefBindings(test.expect)) {
t.Errorf("got %+v, expected %+v", derefBindings(got), derefBindings(test.expect))
}
}
}
func TestIamRolesToMembersMap(t *testing.T) {
table := []struct {
input []*cloudresourcemanager.Binding
expect map[string]map[string]bool
}{
{
input: []*cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-1",
"member-2",
},
},
},
expect: map[string]map[string]bool{
"role-1": map[string]bool{
"member-1": true,
"member-2": true,
},
},
},
{
input: []*cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-1",
"member-2",
"member-1",
"member-2",
},
},
},
expect: map[string]map[string]bool{
"role-1": map[string]bool{
"member-1": true,
"member-2": true,
},
},
},
{
input: []*cloudresourcemanager.Binding{
{
Role: "role-1",
},
},
expect: map[string]map[string]bool{
"role-1": map[string]bool{},
},
},
}
for _, test := range table {
got := rolesToMembersMap(test.input)
if !reflect.DeepEqual(got, test.expect) {
t.Errorf("got %+v, expected %+v", got, test.expect)
}
}
}
func TestIamMergeBindings(t *testing.T) {
table := []struct {
input []*cloudresourcemanager.Binding
expect []cloudresourcemanager.Binding
}{
{
input: []*cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-1",
"member-2",
},
},
{
Role: "role-1",
Members: []string{
"member-3",
},
},
},
expect: []cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-1",
"member-2",
"member-3",
},
},
},
},
{
input: []*cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-3",
"member-4",
},
},
{
Role: "role-1",
Members: []string{
"member-2",
"member-1",
},
},
{
Role: "role-2",
Members: []string{
"member-1",
},
},
{
Role: "role-1",
Members: []string{
"member-5",
},
},
{
Role: "role-3",
Members: []string{
"member-1",
},
},
{
Role: "role-2",
Members: []string{
"member-2",
},
},
},
expect: []cloudresourcemanager.Binding{
{
Role: "role-1",
Members: []string{
"member-1",
"member-2",
"member-3",
"member-4",
"member-5",
},
},
{
Role: "role-2",
Members: []string{
"member-1",
"member-2",
},
},
{
Role: "role-3",
Members: []string{
"member-1",
},
},
},
},
}
for _, test := range table {
got := mergeBindings(test.input)
sort.Sort(Binding(got))
for i, _ := range got {
sort.Strings(got[i].Members)
}
if !reflect.DeepEqual(derefBindings(got), test.expect) {
t.Errorf("\ngot %+v\nexpected %+v", derefBindings(got), test.expect)
}
}
}
func derefBindings(b []*cloudresourcemanager.Binding) []cloudresourcemanager.Binding {
db := make([]cloudresourcemanager.Binding, len(b))
for i, v := range b {
db[i] = *v
}
return db
}
type Binding []*cloudresourcemanager.Binding
func (b Binding) Len() int {
return len(b)
}
func (b Binding) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}
func (b Binding) Less(i, j int) bool {
return b[i].Role < b[j].Role
}
var testAccGoogleProject_basic = `
func testAccGoogleProjectImportExisting(pid string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
id = "%v"
}`
project_id = "%s"
var testAccGoogleProject_policy1 = `
}
`, pid)
}
func testAccGoogleProjectImportExistingWithIam(pid string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
id = "%v"
project_id = "%v"
policy_data = "${data.google_iam_policy.admin.policy_data}"
}
data "google_iam_policy" "admin" {
binding {
role = "roles/storage.objectViewer"
@ -468,4 +96,5 @@ data "google_iam_policy" "admin" {
"user:evandbrown@gmail.com",
]
}
}`
}`, pid)
}

View File

@ -9,6 +9,14 @@ import (
"github.com/hashicorp/terraform/terraform"
)
var (
projectId = multiEnvSearch([]string{
"GOOGLE_PROJECT",
"GCLOUD_PROJECT",
"CLOUDSDK_CORE_PROJECT",
})
)
// Test that a service account resource can be created, updated, and destroyed
func TestAccGoogleServiceAccount_basic(t *testing.T) {
accountId := "a" + acctest.RandString(10)

View File

@ -0,0 +1,64 @@
package google
import (
"fmt"
"log"
"time"
"github.com/hashicorp/terraform/helper/resource"
"google.golang.org/api/cloudresourcemanager/v1"
)
type ResourceManagerOperationWaiter struct {
Service *cloudresourcemanager.Service
Op *cloudresourcemanager.Operation
}
func (w *ResourceManagerOperationWaiter) RefreshFunc() resource.StateRefreshFunc {
return func() (interface{}, string, error) {
op, err := w.Service.Operations.Get(w.Op.Name).Do()
if err != nil {
return nil, "", err
}
log.Printf("[DEBUG] Got %v while polling for operation %s's 'done' status", op.Done, w.Op.Name)
return op, fmt.Sprint(op.Done), nil
}
}
func (w *ResourceManagerOperationWaiter) Conf() *resource.StateChangeConf {
return &resource.StateChangeConf{
Pending: []string{"false"},
Target: []string{"true"},
Refresh: w.RefreshFunc(),
}
}
func resourceManagerOperationWait(config *Config, op *cloudresourcemanager.Operation, activity string) error {
return resourceManagerOperationWaitTime(config, op, activity, 4)
}
func resourceManagerOperationWaitTime(config *Config, op *cloudresourcemanager.Operation, activity string, timeoutMin int) error {
w := &ResourceManagerOperationWaiter{
Service: config.clientResourceManager,
Op: op,
}
state := w.Conf()
state.Delay = 10 * time.Second
state.Timeout = time.Duration(timeoutMin) * time.Minute
state.MinTimeout = 2 * time.Second
opRaw, err := state.WaitForState()
if err != nil {
return fmt.Errorf("Error waiting for %s: %s", activity, err)
}
op = opRaw.(*cloudresourcemanager.Operation)
if op.Error != nil {
return fmt.Errorf("Error code %v, message: %s", op.Error.Code, op.Error.Message)
}
return nil
}

View File

@ -0,0 +1,67 @@
package google
import (
"fmt"
"log"
"time"
"github.com/hashicorp/terraform/helper/resource"
"google.golang.org/api/servicemanagement/v1"
)
type ServiceManagementOperationWaiter struct {
Service *servicemanagement.APIService
Op *servicemanagement.Operation
}
func (w *ServiceManagementOperationWaiter) RefreshFunc() resource.StateRefreshFunc {
return func() (interface{}, string, error) {
var op *servicemanagement.Operation
var err error
op, err = w.Service.Operations.Get(w.Op.Name).Do()
if err != nil {
return nil, "", err
}
log.Printf("[DEBUG] Got %v while polling for operation %s's 'done' status", op.Done, w.Op.Name)
return op, fmt.Sprint(op.Done), nil
}
}
func (w *ServiceManagementOperationWaiter) Conf() *resource.StateChangeConf {
return &resource.StateChangeConf{
Pending: []string{"false"},
Target: []string{"true"},
Refresh: w.RefreshFunc(),
}
}
func serviceManagementOperationWait(config *Config, op *servicemanagement.Operation, activity string) error {
return serviceManagementOperationWaitTime(config, op, activity, 4)
}
func serviceManagementOperationWaitTime(config *Config, op *servicemanagement.Operation, activity string, timeoutMin int) error {
w := &ServiceManagementOperationWaiter{
Service: config.clientServiceMan,
Op: op,
}
state := w.Conf()
state.Delay = 10 * time.Second
state.Timeout = time.Duration(timeoutMin) * time.Minute
state.MinTimeout = 2 * time.Second
opRaw, err := state.WaitForState()
if err != nil {
return fmt.Errorf("Error waiting for %s: %s", activity, err)
}
op = opRaw.(*servicemanagement.Operation)
if op.Error != nil {
return fmt.Errorf("Error code %v, message: %s", op.Error.Code, op.Error.Message)
}
return nil
}

View File

@ -100,8 +100,8 @@ func TestAccHerokuApp_NukeVars(t *testing.T) {
testAccCheckHerokuAppAttributesNoVars(&app, appName),
resource.TestCheckResourceAttr(
"heroku_app.foobar", "name", appName),
resource.TestCheckResourceAttr(
"heroku_app.foobar", "config_vars.0.FOO", ""),
resource.TestCheckNoResourceAttr(
"heroku_app.foobar", "config_vars.0.FOO"),
),
},
},

View File

@ -83,7 +83,7 @@ func testIgnition(t *testing.T, input string, assert func(*types.Config) error)
resource.Test(t, resource.TestCase{
Providers: testProviders,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: fmt.Sprintf(testTemplate, input),
Check: check,
},

View File

@ -12,39 +12,39 @@ func resourceSystemdUnit() *schema.Resource {
Exists: resourceSystemdUnitExists,
Read: resourceSystemdUnitRead,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"enable": &schema.Schema{
"enable": {
Type: schema.TypeBool,
Optional: true,
Default: true,
ForceNew: true,
},
"mask": &schema.Schema{
"mask": {
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"content": &schema.Schema{
"content": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"dropin": &schema.Schema{
"dropin": {
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"content": &schema.Schema{
"content": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
@ -100,7 +100,7 @@ func buildSystemdUnit(d *schema.ResourceData, c *cache) (string, error) {
}
if err := validateUnitContent(d.Get("content").(string)); err != nil {
if err != errEmptyUnit || (err == errEmptyUnit && len(dropins) == 0) {
if err != errEmptyUnit {
return "", err
}
}

View File

@ -94,3 +94,35 @@ func TestIngnitionSystemdUnitEmptyContentWithDropIn(t *testing.T) {
return nil
})
}
// #11325
func TestIgnitionSystemdUnit_emptyContent(t *testing.T) {
testIgnition(t, `
resource "ignition_systemd_unit" "foo" {
name = "foo.service"
enable = true
}
resource "ignition_config" "test" {
systemd = [
"${ignition_systemd_unit.foo.id}",
]
}
`, func(c *types.Config) error {
if len(c.Systemd.Units) != 1 {
return fmt.Errorf("systemd, found %d", len(c.Systemd.Units))
}
u := c.Systemd.Units[0]
if u.Name != "foo.service" {
return fmt.Errorf("name, expected 'foo.service', found %q", u.Name)
}
if u.Contents != "" {
return fmt.Errorf("expected empty content, found %q", u.Contents)
}
if len(u.DropIns) != 0 {
return fmt.Errorf("expected 0 dropins, found %q", u.DropIns)
}
return nil
})
}

View File

@ -0,0 +1,218 @@
package ns1
import (
"fmt"
"reflect"
"github.com/hashicorp/terraform/helper/schema"
"gopkg.in/ns1/ns1-go.v2/rest/model/data"
)
type TfSchemaBuilder func(*schema.Schema)
func mtSimple(t schema.ValueType) TfSchemaBuilder {
return func(s *schema.Schema) {
s.Type = t
}
}
func mtStringEnum(se *StringEnum) TfSchemaBuilder {
return func(s *schema.Schema) {
s.Type = schema.TypeString
s.ValidateFunc = func(v interface{}, k string) ([]string, []error) {
_, err := se.Check(v.(string))
if err != nil {
return nil, []error{err}
}
return nil, nil
}
}
}
var mtInt TfSchemaBuilder = mtSimple(schema.TypeInt)
var mtBool TfSchemaBuilder = mtSimple(schema.TypeBool)
var mtString TfSchemaBuilder = mtSimple(schema.TypeString)
var mtFloat64 TfSchemaBuilder = mtSimple(schema.TypeFloat)
func mtList(elementSchemaBuilder TfSchemaBuilder) TfSchemaBuilder {
return func(s *schema.Schema) {
s.Type = schema.TypeList
elementSchema := &schema.Schema{}
elementSchemaBuilder(elementSchema)
s.Elem = elementSchema
}
}
var mtStringList TfSchemaBuilder = mtList(mtString)
type MetaFieldSpec struct {
NameInDynamic string
NameInStruct string
SchemaBuilder TfSchemaBuilder
}
type MetaField struct {
MetaFieldSpec
NameInDynamicForFeed string
StructIndex int
StructGoType reflect.Type
}
var georegionEnum *StringEnum = NewStringEnum([]string{
"US-WEST",
"US-EAST",
"US-CENTRAL",
"EUROPE",
"AFRICA",
"ASIAPAC",
"SOUTH-AMERICA",
})
func makeMetaFields() []MetaField {
var specs []MetaFieldSpec = []MetaFieldSpec{
{"up", "Up", mtBool},
{"connections", "Connections", mtInt},
{"requests", "Requests", mtInt},
{"loadavg", "LoadAvg", mtFloat64},
{"pulsar", "Pulsar", mtInt},
{"latitude", "Latitude", mtFloat64},
{"longitude", "Longitude", mtFloat64},
{"georegion", "Georegion", mtList(mtStringEnum(georegionEnum))},
{"country", "Country", mtStringList},
{"us_state", "USState", mtStringList},
{"ca_province", "CAProvince", mtStringList},
{"note", "Note", mtString},
{"ip_prefixes", "IPPrefixes", mtStringList},
{"asn", "ASN", mtList(mtInt)},
{"priority", "Priority", mtInt},
{"weight", "Weight", mtFloat64},
{"low_watermark", "LowWatermark", mtInt},
{"high_watermark", "HighWatermark", mtInt},
}
// Figure out the field indexes (in data.Meta) for all the fields.
// This way we can later lookup by index, which should be faster than by name.
rt := reflect.TypeOf(data.Meta{})
fields := make([]MetaField, len(specs))
for i, spec := range specs {
rf, present := rt.FieldByName(spec.NameInStruct)
if !present {
panic(fmt.Sprintf("Field %q not present", spec.NameInStruct))
}
if len(rf.Index) != 1 {
panic(fmt.Sprintf("Expecting a single index, got %#v", rf.Index))
}
index := rf.Index[0]
fields[i] = MetaField{
MetaFieldSpec: spec,
StructIndex: index,
NameInDynamicForFeed: spec.NameInDynamic + "_feed",
StructGoType: rf.Type,
}
}
return fields
}
var metaFields []MetaField = makeMetaFields()
func makeMetaSchema() *schema.Schema {
fields := make(map[string]*schema.Schema)
for _, f := range metaFields {
fieldSchema := &schema.Schema{
Optional: true,
ForceNew: true,
// TODO: Fields that arent in configuration shouldnt show up in resource data
// ConflictsWith: []string{f.NameInDynamicForFeed},
}
f.SchemaBuilder(fieldSchema)
fields[f.NameInDynamic] = fieldSchema
// Add an "_feed"-suffixed field for the {"feed":...} value.
fields[f.NameInDynamicForFeed] = &schema.Schema{
Optional: true,
ForceNew: true,
// TODO: Fields that arent in configuration shouldnt show up in resource data
// ConflictsWith: []string{f.NameInDynamic},
Type: schema.TypeString,
}
}
metaSchemaInner := &schema.Resource{
Schema: fields,
}
// Wrap it in a list because that seems to be the only way to have nested structs.
return &schema.Schema{
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: metaSchemaInner,
}
}
var metaSchema *schema.Schema = makeMetaSchema()
func metaStructToDynamic(m *data.Meta) interface{} {
d := make(map[string]interface{})
mr := reflect.ValueOf(m).Elem()
for _, f := range metaFields {
fr := mr.Field(f.StructIndex)
fv := fr.Interface()
if fv == nil {
continue
}
if mapVal, isMap := fv.(map[string]interface{}); isMap {
if len(mapVal) == 1 {
if feedVal, ok := mapVal["feed"]; ok {
if feedStr, ok := feedVal.(string); ok {
d[f.NameInDynamicForFeed] = feedStr
continue
}
}
}
panic(fmt.Sprintf("expecting feed dict, got %+v", mapVal))
}
d[f.NameInDynamic] = fv
}
return []interface{}{d}
}
func metaDynamicToStruct(m *data.Meta, raw interface{}) {
l := raw.([]interface{})
if len(l) > 1 {
panic(fmt.Sprintf("list too long %#v", l))
}
if len(l) == 0 {
return
}
if l[0] == nil {
return
}
d := l[0].(map[string]interface{})
mr := reflect.ValueOf(m).Elem()
for _, f := range metaFields {
val, present := d[f.NameInDynamic]
if present {
fr := mr.Field(f.StructIndex)
fr.Set(reflect.ValueOf(val))
}
feed, present := d[f.NameInDynamicForFeed]
if present && feed != "" {
if feed == nil {
panic("unexpected nil")
}
fr := mr.Field(f.StructIndex)
fr.Set(reflect.ValueOf(map[string]interface{}{"feed": feed.(string)}))
}
}
}

View File

@ -0,0 +1,184 @@
package ns1
import (
"github.com/hashicorp/terraform/helper/schema"
"gopkg.in/ns1/ns1-go.v2/rest/model/account"
)
func addPermsSchema(s map[string]*schema.Schema) map[string]*schema.Schema {
s["dns_view_zones"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["dns_manage_zones"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["dns_zones_allow_by_default"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["dns_zones_deny"] = &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
}
s["dns_zones_allow"] = &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
}
s["data_push_to_datafeeds"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["data_manage_datasources"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["data_manage_datafeeds"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["account_manage_users"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["account_manage_payment_methods"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["account_manage_plan"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["account_manage_teams"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["account_manage_apikeys"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["account_manage_account_settings"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["account_view_activity_log"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["account_view_invoices"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["monitoring_manage_lists"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["monitoring_manage_jobs"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
s["monitoring_view_jobs"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
}
return s
}
func permissionsToResourceData(d *schema.ResourceData, permissions account.PermissionsMap) {
d.Set("dns_view_zones", permissions.DNS.ViewZones)
d.Set("dns_manage_zones", permissions.DNS.ManageZones)
d.Set("dns_zones_allow_by_default", permissions.DNS.ZonesAllowByDefault)
d.Set("dns_zones_deny", permissions.DNS.ZonesDeny)
d.Set("dns_zones_allow", permissions.DNS.ZonesAllow)
d.Set("data_push_to_datafeeds", permissions.Data.PushToDatafeeds)
d.Set("data_manage_datasources", permissions.Data.ManageDatasources)
d.Set("data_manage_datafeeds", permissions.Data.ManageDatafeeds)
d.Set("account_manage_users", permissions.Account.ManageUsers)
d.Set("account_manage_payment_methods", permissions.Account.ManagePaymentMethods)
d.Set("account_manage_plan", permissions.Account.ManagePlan)
d.Set("account_manage_teams", permissions.Account.ManageTeams)
d.Set("account_manage_apikeys", permissions.Account.ManageApikeys)
d.Set("account_manage_account_settings", permissions.Account.ManageAccountSettings)
d.Set("account_view_activity_log", permissions.Account.ViewActivityLog)
d.Set("account_view_invoices", permissions.Account.ViewInvoices)
d.Set("monitoring_manage_lists", permissions.Monitoring.ManageLists)
d.Set("monitoring_manage_jobs", permissions.Monitoring.ManageJobs)
d.Set("monitoring_view_jobs", permissions.Monitoring.ViewJobs)
}
func resourceDataToPermissions(d *schema.ResourceData) account.PermissionsMap {
var p account.PermissionsMap
if v, ok := d.GetOk("dns_view_zones"); ok {
p.DNS.ViewZones = v.(bool)
}
if v, ok := d.GetOk("dns_manage_zones"); ok {
p.DNS.ManageZones = v.(bool)
}
if v, ok := d.GetOk("dns_zones_allow_by_default"); ok {
p.DNS.ZonesAllowByDefault = v.(bool)
}
if v, ok := d.GetOk("dns_zones_deny"); ok {
denyRaw := v.([]interface{})
p.DNS.ZonesDeny = make([]string, len(denyRaw))
for i, deny := range denyRaw {
p.DNS.ZonesDeny[i] = deny.(string)
}
} else {
p.DNS.ZonesDeny = make([]string, 0)
}
if v, ok := d.GetOk("dns_zones_allow"); ok {
allowRaw := v.([]interface{})
p.DNS.ZonesAllow = make([]string, len(allowRaw))
for i, allow := range allowRaw {
p.DNS.ZonesAllow[i] = allow.(string)
}
} else {
p.DNS.ZonesAllow = make([]string, 0)
}
if v, ok := d.GetOk("data_push_to_datafeeds"); ok {
p.Data.PushToDatafeeds = v.(bool)
}
if v, ok := d.GetOk("data_manage_datasources"); ok {
p.Data.ManageDatasources = v.(bool)
}
if v, ok := d.GetOk("data_manage_datafeeds"); ok {
p.Data.ManageDatafeeds = v.(bool)
}
if v, ok := d.GetOk("account_manage_users"); ok {
p.Account.ManageUsers = v.(bool)
}
if v, ok := d.GetOk("account_manage_payment_methods"); ok {
p.Account.ManagePaymentMethods = v.(bool)
}
if v, ok := d.GetOk("account_manage_plan"); ok {
p.Account.ManagePlan = v.(bool)
}
if v, ok := d.GetOk("account_manage_teams"); ok {
p.Account.ManageTeams = v.(bool)
}
if v, ok := d.GetOk("account_manage_apikeys"); ok {
p.Account.ManageApikeys = v.(bool)
}
if v, ok := d.GetOk("account_manage_account_settings"); ok {
p.Account.ManageAccountSettings = v.(bool)
}
if v, ok := d.GetOk("account_view_activity_log"); ok {
p.Account.ViewActivityLog = v.(bool)
}
if v, ok := d.GetOk("account_view_invoices"); ok {
p.Account.ViewInvoices = v.(bool)
}
if v, ok := d.GetOk("monitoring_manage_lists"); ok {
p.Monitoring.ManageLists = v.(bool)
}
if v, ok := d.GetOk("monitoring_manage_jobs"); ok {
p.Monitoring.ManageJobs = v.(bool)
}
if v, ok := d.GetOk("monitoring_view_jobs"); ok {
p.Monitoring.ViewJobs = v.(bool)
}
return p
}

View File

@ -0,0 +1,50 @@
package ns1
import (
"net/http"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
)
// Provider returns a terraform.ResourceProvider.
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"apikey": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("NS1_APIKEY", nil),
Description: descriptions["api_key"],
},
},
ResourcesMap: map[string]*schema.Resource{
"ns1_zone": zoneResource(),
"ns1_record": recordResource(),
"ns1_datasource": dataSourceResource(),
"ns1_datafeed": dataFeedResource(),
"ns1_monitoringjob": monitoringJobResource(),
"ns1_user": userResource(),
"ns1_apikey": apikeyResource(),
"ns1_team": teamResource(),
},
ConfigureFunc: ns1Configure,
}
}
func ns1Configure(d *schema.ResourceData) (interface{}, error) {
httpClient := &http.Client{}
n := ns1.NewClient(httpClient, ns1.SetAPIKey(d.Get("apikey").(string)))
n.RateLimitStrategySleep()
return n, nil
}
var descriptions map[string]string
func init() {
descriptions = map[string]string{
"api_key": "The ns1 API key, this is required",
}
}

View File

@ -0,0 +1,35 @@
package ns1
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{
"ns1": 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("NS1_APIKEY"); v == "" {
t.Fatal("NS1_APIKEY must be set for acceptance tests")
}
}

View File

@ -0,0 +1,109 @@
package ns1
import (
"github.com/hashicorp/terraform/helper/schema"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/account"
)
func apikeyResource() *schema.Resource {
s := map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"key": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"teams": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
}
s = addPermsSchema(s)
return &schema.Resource{
Schema: s,
Create: ApikeyCreate,
Read: ApikeyRead,
Update: ApikeyUpdate,
Delete: ApikeyDelete,
}
}
func apikeyToResourceData(d *schema.ResourceData, k *account.APIKey) error {
d.SetId(k.ID)
d.Set("name", k.Name)
d.Set("key", k.Key)
d.Set("teams", k.TeamIDs)
permissionsToResourceData(d, k.Permissions)
return nil
}
func resourceDataToApikey(k *account.APIKey, d *schema.ResourceData) error {
k.ID = d.Id()
k.Name = d.Get("name").(string)
if v, ok := d.GetOk("teams"); ok {
teamsRaw := v.([]interface{})
k.TeamIDs = make([]string, len(teamsRaw))
for i, team := range teamsRaw {
k.TeamIDs[i] = team.(string)
}
} else {
k.TeamIDs = make([]string, 0)
}
k.Permissions = resourceDataToPermissions(d)
return nil
}
// ApikeyCreate creates ns1 API key
func ApikeyCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
k := account.APIKey{}
if err := resourceDataToApikey(&k, d); err != nil {
return err
}
if _, err := client.APIKeys.Create(&k); err != nil {
return err
}
return apikeyToResourceData(d, &k)
}
// ApikeyRead reads API key from ns1
func ApikeyRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
k, _, err := client.APIKeys.Get(d.Id())
if err != nil {
return err
}
return apikeyToResourceData(d, k)
}
//ApikeyDelete deletes the given ns1 api key
func ApikeyDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
_, err := client.APIKeys.Delete(d.Id())
d.SetId("")
return err
}
//ApikeyUpdate updates the given api key in ns1
func ApikeyUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
k := account.APIKey{
ID: d.Id(),
}
if err := resourceDataToApikey(&k, d); err != nil {
return err
}
if _, err := client.APIKeys.Update(&k); err != nil {
return err
}
return apikeyToResourceData(d, &k)
}

View File

@ -0,0 +1,92 @@
package ns1
import (
"github.com/hashicorp/terraform/helper/schema"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/data"
)
func dataFeedResource() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"source_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"config": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
},
},
Create: DataFeedCreate,
Read: DataFeedRead,
Update: DataFeedUpdate,
Delete: DataFeedDelete,
}
}
func dataFeedToResourceData(d *schema.ResourceData, f *data.Feed) {
d.SetId(f.ID)
d.Set("name", f.Name)
d.Set("config", f.Config)
}
func resourceDataToDataFeed(d *schema.ResourceData) *data.Feed {
return &data.Feed{
Name: d.Get("name").(string),
SourceID: d.Get("source_id").(string),
Config: d.Get("config").(map[string]interface{}),
}
}
// DataFeedCreate creates an ns1 datafeed
func DataFeedCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
f := resourceDataToDataFeed(d)
if _, err := client.DataFeeds.Create(d.Get("source_id").(string), f); err != nil {
return err
}
dataFeedToResourceData(d, f)
return nil
}
// DataFeedRead reads the datafeed for the given ID from ns1
func DataFeedRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
f, _, err := client.DataFeeds.Get(d.Get("source_id").(string), d.Id())
if err != nil {
return err
}
dataFeedToResourceData(d, f)
return nil
}
// DataFeedDelete delets the given datafeed from ns1
func DataFeedDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
_, err := client.DataFeeds.Delete(d.Get("source_id").(string), d.Id())
d.SetId("")
return err
}
// DataFeedUpdate updates the given datafeed with modified parameters
func DataFeedUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
f := resourceDataToDataFeed(d)
f.ID = d.Id()
if _, err := client.DataFeeds.Update(d.Get("source_id").(string), f); err != nil {
return err
}
dataFeedToResourceData(d, f)
return nil
}

View File

@ -0,0 +1,170 @@
package ns1
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/data"
)
func TestAccDataFeed_basic(t *testing.T) {
var dataFeed data.Feed
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDataFeedDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDataFeedBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckDataFeedExists("ns1_datafeed.foobar", "ns1_datasource.api", &dataFeed),
testAccCheckDataFeedName(&dataFeed, "terraform test"),
testAccCheckDataFeedConfig(&dataFeed, "label", "exampledc2"),
),
},
},
})
}
func TestAccDataFeed_updated(t *testing.T) {
var dataFeed data.Feed
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDataFeedDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDataFeedBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckDataFeedExists("ns1_datafeed.foobar", "ns1_datasource.api", &dataFeed),
testAccCheckDataFeedName(&dataFeed, "terraform test"),
testAccCheckDataFeedConfig(&dataFeed, "label", "exampledc2"),
),
},
resource.TestStep{
Config: testAccDataFeedUpdated,
Check: resource.ComposeTestCheckFunc(
testAccCheckDataFeedExists("ns1_datafeed.foobar", "ns1_datasource.api", &dataFeed),
testAccCheckDataFeedName(&dataFeed, "terraform test"),
testAccCheckDataFeedConfig(&dataFeed, "label", "exampledc3"),
),
},
},
})
}
func testAccCheckDataFeedExists(n string, dsrc string, dataFeed *data.Feed) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
ds, ok := s.RootModule().Resources[dsrc]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("NoID is set")
}
if ds.Primary.ID == "" {
return fmt.Errorf("NoID is set for the datasource")
}
client := testAccProvider.Meta().(*ns1.Client)
foundFeed, _, err := client.DataFeeds.Get(ds.Primary.Attributes["id"], rs.Primary.Attributes["id"])
p := rs.Primary
if err != nil {
return err
}
if foundFeed.Name != p.Attributes["name"] {
return fmt.Errorf("DataFeed not found")
}
*dataFeed = *foundFeed
return nil
}
}
func testAccCheckDataFeedDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*ns1.Client)
var dataFeedID string
var dataSourceID string
for _, rs := range s.RootModule().Resources {
if rs.Type == "ns1_datasource" {
dataSourceID = rs.Primary.Attributes["id"]
}
if rs.Type == "ns1_datafeed" {
dataFeedID = rs.Primary.Attributes["id"]
}
}
df, _, _ := client.DataFeeds.Get(dataSourceID, dataFeedID)
if df != nil {
return fmt.Errorf("DataFeed still exists: %#v", df)
}
return nil
}
func testAccCheckDataFeedName(dataFeed *data.Feed, expected string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if dataFeed.Name != expected {
return fmt.Errorf("Name: got: %#v want: %#v", dataFeed.Name, expected)
}
return nil
}
}
func testAccCheckDataFeedConfig(dataFeed *data.Feed, key, expected string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if dataFeed.Config[key] != expected {
return fmt.Errorf("Config[%s]: got: %#v, want: %s", key, dataFeed.Config[key], expected)
}
return nil
}
}
const testAccDataFeedBasic = `
resource "ns1_datasource" "api" {
name = "terraform test"
sourcetype = "nsone_v1"
}
resource "ns1_datafeed" "foobar" {
name = "terraform test"
source_id = "${ns1_datasource.api.id}"
config {
label = "exampledc2"
}
}`
const testAccDataFeedUpdated = `
resource "ns1_datasource" "api" {
name = "terraform test"
sourcetype = "nsone_v1"
}
resource "ns1_datafeed" "foobar" {
name = "terraform test"
source_id = "${ns1_datasource.api.id}"
config {
label = "exampledc3"
}
}`

View File

@ -0,0 +1,86 @@
package ns1
import (
"github.com/hashicorp/terraform/helper/schema"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/data"
)
func dataSourceResource() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"sourcetype": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"config": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
},
},
Create: DataSourceCreate,
Read: DataSourceRead,
Update: DataSourceUpdate,
Delete: DataSourceDelete,
}
}
func dataSourceToResourceData(d *schema.ResourceData, s *data.Source) {
d.SetId(s.ID)
d.Set("name", s.Name)
d.Set("sourcetype", s.Type)
d.Set("config", s.Config)
}
// DataSourceCreate creates an ns1 datasource
func DataSourceCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
s := data.NewSource(d.Get("name").(string), d.Get("sourcetype").(string))
s.Config = d.Get("config").(map[string]interface{})
if _, err := client.DataSources.Create(s); err != nil {
return err
}
dataSourceToResourceData(d, s)
return nil
}
// DataSourceRead fetches info for the given datasource from ns1
func DataSourceRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
s, _, err := client.DataSources.Get(d.Id())
if err != nil {
return err
}
dataSourceToResourceData(d, s)
return nil
}
// DataSourceDelete deteltes the given datasource from ns1
func DataSourceDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
_, err := client.DataSources.Delete(d.Id())
d.SetId("")
return err
}
// DataSourceUpdate updates the datasource with given parameters
func DataSourceUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
s := data.NewSource(d.Get("name").(string), d.Get("sourcetype").(string))
s.ID = d.Id()
if _, err := client.DataSources.Update(s); err != nil {
return err
}
dataSourceToResourceData(d, s)
return nil
}

View File

@ -0,0 +1,140 @@
package ns1
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/data"
)
func TestAccDataSource_basic(t *testing.T) {
var dataSource data.Source
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDataSourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDataSourceBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckDataSourceExists("ns1_datasource.foobar", &dataSource),
testAccCheckDataSourceName(&dataSource, "terraform test"),
testAccCheckDataSourceType(&dataSource, "nsone_v1"),
),
},
},
})
}
func TestAccDataSource_updated(t *testing.T) {
var dataSource data.Source
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDataSourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDataSourceBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckDataSourceExists("ns1_datasource.foobar", &dataSource),
testAccCheckDataSourceName(&dataSource, "terraform test"),
testAccCheckDataSourceType(&dataSource, "nsone_v1"),
),
},
resource.TestStep{
Config: testAccDataSourceUpdated,
Check: resource.ComposeTestCheckFunc(
testAccCheckDataSourceExists("ns1_datasource.foobar", &dataSource),
testAccCheckDataSourceName(&dataSource, "terraform test"),
testAccCheckDataSourceType(&dataSource, "nsone_monitoring"),
),
},
},
})
}
func testAccCheckDataSourceExists(n string, dataSource *data.Source) 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("NoID is set")
}
client := testAccProvider.Meta().(*ns1.Client)
foundSource, _, err := client.DataSources.Get(rs.Primary.Attributes["id"])
p := rs.Primary
if err != nil {
return err
}
if foundSource.Name != p.Attributes["name"] {
return fmt.Errorf("Datasource not found")
}
*dataSource = *foundSource
return nil
}
}
func testAccCheckDataSourceDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*ns1.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "ns1_datasource" {
continue
}
_, _, err := client.DataSources.Get(rs.Primary.Attributes["id"])
if err == nil {
return fmt.Errorf("Datasource still exists")
}
}
return nil
}
func testAccCheckDataSourceName(dataSource *data.Source, expected string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if dataSource.Name != expected {
return fmt.Errorf("Name: got: %#v want: %#v", dataSource.Name, expected)
}
return nil
}
}
func testAccCheckDataSourceType(dataSource *data.Source, expected string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if dataSource.Type != expected {
return fmt.Errorf("Type: got: %#v want: %#v", dataSource.Type, expected)
}
return nil
}
}
const testAccDataSourceBasic = `
resource "ns1_datasource" "foobar" {
name = "terraform test"
sourcetype = "nsone_v1"
}`
const testAccDataSourceUpdated = `
resource "ns1_datasource" "foobar" {
name = "terraform test"
sourcetype = "nsone_monitoring"
}`

View File

@ -0,0 +1,297 @@
package ns1
import (
"fmt"
"regexp"
"strconv"
"github.com/hashicorp/terraform/helper/schema"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/monitor"
)
func monitoringJobResource() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
// Required
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"job_type": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"regions": &schema.Schema{
Type: schema.TypeList,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"frequency": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"config": &schema.Schema{
Type: schema.TypeMap,
Required: true,
},
// Optional
"active": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"rapid_recheck": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"policy": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "quorum",
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
value := v.(string)
if !regexp.MustCompile(`^(all|one|quorum)$`).MatchString(value) {
es = append(es, fmt.Errorf(
"only all, one, quorum allowed in %q", k))
}
return
},
},
"notes": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"notify_delay": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
"notify_repeat": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
},
"notify_failback": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"notify_regional": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"notify_list": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"rules": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"value": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"comparison": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"key": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},
},
},
// Computed
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
Create: MonitoringJobCreate,
Read: MonitoringJobRead,
Update: MonitoringJobUpdate,
Delete: MonitoringJobDelete,
}
}
func monitoringJobToResourceData(d *schema.ResourceData, r *monitor.Job) error {
d.SetId(r.ID)
d.Set("name", r.Name)
d.Set("job_type", r.Type)
d.Set("active", r.Active)
d.Set("regions", r.Regions)
d.Set("frequency", r.Frequency)
d.Set("rapid_recheck", r.RapidRecheck)
config := make(map[string]string)
for k, v := range r.Config {
if k == "ssl" {
if v.(bool) {
config[k] = "1"
} else {
config[k] = "0"
}
} else {
switch t := v.(type) {
case string:
config[k] = t
case float64:
config[k] = strconv.FormatFloat(t, 'f', -1, 64)
}
}
}
err := d.Set("config", config)
if err != nil {
panic(fmt.Errorf("[DEBUG] Error setting Config error: %#v %#v", r.Config, err))
}
d.Set("policy", r.Policy)
d.Set("notes", r.Notes)
d.Set("frequency", r.Frequency)
d.Set("notify_delay", r.NotifyDelay)
d.Set("notify_repeat", r.NotifyRepeat)
d.Set("notify_regional", r.NotifyRegional)
d.Set("notify_failback", r.NotifyFailback)
d.Set("notify_list", r.NotifyListID)
if len(r.Rules) > 0 {
rules := make([]map[string]interface{}, len(r.Rules))
for i, r := range r.Rules {
m := make(map[string]interface{})
m["value"] = r.Value
m["comparison"] = r.Comparison
m["key"] = r.Key
rules[i] = m
}
}
return nil
}
func resourceDataToMonitoringJob(r *monitor.Job, d *schema.ResourceData) error {
r.ID = d.Id()
r.Name = d.Get("name").(string)
r.Type = d.Get("job_type").(string)
r.Active = d.Get("active").(bool)
rawRegions := d.Get("regions").([]interface{})
r.Regions = make([]string, len(rawRegions))
for i, v := range rawRegions {
r.Regions[i] = v.(string)
}
r.Frequency = d.Get("frequency").(int)
r.RapidRecheck = d.Get("rapid_recheck").(bool)
var rawRules []interface{}
if rawRules := d.Get("rules"); rawRules != nil {
r.Rules = make([]*monitor.Rule, len(rawRules.([]interface{})))
for i, v := range rawRules.([]interface{}) {
rule := v.(map[string]interface{})
r.Rules[i] = &monitor.Rule{
Value: rule["value"].(string),
Comparison: rule["comparison"].(string),
Key: rule["key"].(string),
}
}
} else {
r.Rules = make([]*monitor.Rule, 0)
}
for i, v := range rawRules {
rule := v.(map[string]interface{})
r.Rules[i] = &monitor.Rule{
Comparison: rule["comparison"].(string),
Key: rule["key"].(string),
}
value := rule["value"].(string)
if i, err := strconv.Atoi(value); err == nil {
r.Rules[i].Value = i
} else {
r.Rules[i].Value = value
}
}
config := make(map[string]interface{})
if rawConfig := d.Get("config"); rawConfig != nil {
for k, v := range rawConfig.(map[string]interface{}) {
if k == "ssl" {
if v.(string) == "1" {
config[k] = true
}
} else {
if i, err := strconv.Atoi(v.(string)); err == nil {
config[k] = i
} else {
config[k] = v
}
}
}
}
r.Config = config
r.RegionScope = "fixed"
r.Policy = d.Get("policy").(string)
if v, ok := d.GetOk("notes"); ok {
r.Notes = v.(string)
}
r.Frequency = d.Get("frequency").(int)
if v, ok := d.GetOk("notify_delay"); ok {
r.NotifyDelay = v.(int)
}
if v, ok := d.GetOk("notify_repeat"); ok {
r.NotifyRepeat = v.(int)
}
if v, ok := d.GetOk("notify_regional"); ok {
r.NotifyRegional = v.(bool)
}
if v, ok := d.GetOk("notify_failback"); ok {
r.NotifyFailback = v.(bool)
}
if v, ok := d.GetOk("notify_list"); ok {
r.NotifyListID = v.(string)
}
return nil
}
// MonitoringJobCreate Creates monitoring job in ns1
func MonitoringJobCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
j := monitor.Job{}
if err := resourceDataToMonitoringJob(&j, d); err != nil {
return err
}
if _, err := client.Jobs.Create(&j); err != nil {
return err
}
return monitoringJobToResourceData(d, &j)
}
// MonitoringJobRead reads the given monitoring job from ns1
func MonitoringJobRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
j, _, err := client.Jobs.Get(d.Id())
if err != nil {
return err
}
return monitoringJobToResourceData(d, j)
}
// MonitoringJobDelete deteltes the given monitoring job from ns1
func MonitoringJobDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
_, err := client.Jobs.Delete(d.Id())
d.SetId("")
return err
}
// MonitoringJobUpdate updates the given monitoring job
func MonitoringJobUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
j := monitor.Job{
ID: d.Id(),
}
if err := resourceDataToMonitoringJob(&j, d); err != nil {
return err
}
if _, err := client.Jobs.Update(&j); err != nil {
return err
}
return monitoringJobToResourceData(d, &j)
}

View File

@ -0,0 +1,278 @@
package ns1
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/monitor"
)
func TestAccMonitoringJob_basic(t *testing.T) {
var mj monitor.Job
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckMonitoringJobDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccMonitoringJobBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckMonitoringJobExists("ns1_monitoringjob.it", &mj),
testAccCheckMonitoringJobName(&mj, "terraform test"),
testAccCheckMonitoringJobActive(&mj, true),
testAccCheckMonitoringJobRegions(&mj, []string{"lga"}),
testAccCheckMonitoringJobType(&mj, "tcp"),
testAccCheckMonitoringJobFrequency(&mj, 60),
testAccCheckMonitoringJobRapidRecheck(&mj, false),
testAccCheckMonitoringJobPolicy(&mj, "quorum"),
testAccCheckMonitoringJobConfigSend(&mj, "HEAD / HTTP/1.0\r\n\r\n"),
testAccCheckMonitoringJobConfigPort(&mj, 80),
testAccCheckMonitoringJobConfigHost(&mj, "1.1.1.1"),
),
},
},
})
}
func TestAccMonitoringJob_updated(t *testing.T) {
var mj monitor.Job
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckMonitoringJobDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccMonitoringJobBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckMonitoringJobExists("ns1_monitoringjob.it", &mj),
testAccCheckMonitoringJobName(&mj, "terraform test"),
testAccCheckMonitoringJobActive(&mj, true),
testAccCheckMonitoringJobRegions(&mj, []string{"lga"}),
testAccCheckMonitoringJobType(&mj, "tcp"),
testAccCheckMonitoringJobFrequency(&mj, 60),
testAccCheckMonitoringJobRapidRecheck(&mj, false),
testAccCheckMonitoringJobPolicy(&mj, "quorum"),
testAccCheckMonitoringJobConfigSend(&mj, "HEAD / HTTP/1.0\r\n\r\n"),
testAccCheckMonitoringJobConfigPort(&mj, 80),
testAccCheckMonitoringJobConfigHost(&mj, "1.1.1.1"),
),
},
resource.TestStep{
Config: testAccMonitoringJobUpdated,
Check: resource.ComposeTestCheckFunc(
testAccCheckMonitoringJobExists("ns1_monitoringjob.it", &mj),
testAccCheckMonitoringJobName(&mj, "terraform test"),
testAccCheckMonitoringJobActive(&mj, true),
testAccCheckMonitoringJobRegions(&mj, []string{"lga"}),
testAccCheckMonitoringJobType(&mj, "tcp"),
testAccCheckMonitoringJobFrequency(&mj, 120),
testAccCheckMonitoringJobRapidRecheck(&mj, true),
testAccCheckMonitoringJobPolicy(&mj, "all"),
testAccCheckMonitoringJobConfigSend(&mj, "HEAD / HTTP/1.0\r\n\r\n"),
testAccCheckMonitoringJobConfigPort(&mj, 443),
testAccCheckMonitoringJobConfigHost(&mj, "1.1.1.1"),
),
},
},
})
}
func testAccCheckMonitoringJobState(key, value string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources["ns1_monitoringjob.it"]
if !ok {
return fmt.Errorf("Not found: %s", "ns1_monitoringjob.it")
}
if rs.Primary.ID == "" {
return fmt.Errorf("No ID is set")
}
p := rs.Primary
if p.Attributes[key] != value {
return fmt.Errorf(
"%s != %s (actual: %s)", key, value, p.Attributes[key])
}
return nil
}
}
func testAccCheckMonitoringJobExists(n string, monitoringJob *monitor.Job) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Resource not found: %v", n)
}
id := rs.Primary.ID
if id == "" {
return fmt.Errorf("ID is not set")
}
client := testAccProvider.Meta().(*ns1.Client)
foundMj, _, err := client.Jobs.Get(id)
if err != nil {
return err
}
if foundMj.ID != id {
return fmt.Errorf("Monitoring Job not found want: %#v, got %#v", id, foundMj)
}
*monitoringJob = *foundMj
return nil
}
}
func testAccCheckMonitoringJobDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*ns1.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "ns1_monitoringjob" {
continue
}
mj, _, err := client.Jobs.Get(rs.Primary.Attributes["id"])
if err == nil {
return fmt.Errorf("Monitoring Job still exists %#v: %#v", err, mj)
}
}
return nil
}
func testAccCheckMonitoringJobName(mj *monitor.Job, expected string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if mj.Name != expected {
return fmt.Errorf("Name: got: %#v want: %#v", mj.Name, expected)
}
return nil
}
}
func testAccCheckMonitoringJobActive(mj *monitor.Job, expected bool) resource.TestCheckFunc {
return func(s *terraform.State) error {
if mj.Active != expected {
return fmt.Errorf("Active: got: %#v want: %#v", mj.Active, expected)
}
return nil
}
}
func testAccCheckMonitoringJobRegions(mj *monitor.Job, expected []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if !reflect.DeepEqual(mj.Regions, expected) {
return fmt.Errorf("Regions: got: %#v want: %#v", mj.Regions, expected)
}
return nil
}
}
func testAccCheckMonitoringJobType(mj *monitor.Job, expected string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if mj.Type != expected {
return fmt.Errorf("Type: got: %#v want: %#v", mj.Type, expected)
}
return nil
}
}
func testAccCheckMonitoringJobFrequency(mj *monitor.Job, expected int) resource.TestCheckFunc {
return func(s *terraform.State) error {
if mj.Frequency != expected {
return fmt.Errorf("Frequency: got: %#v want: %#v", mj.Frequency, expected)
}
return nil
}
}
func testAccCheckMonitoringJobRapidRecheck(mj *monitor.Job, expected bool) resource.TestCheckFunc {
return func(s *terraform.State) error {
if mj.RapidRecheck != expected {
return fmt.Errorf("RapidRecheck: got: %#v want: %#v", mj.RapidRecheck, expected)
}
return nil
}
}
func testAccCheckMonitoringJobPolicy(mj *monitor.Job, expected string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if mj.Policy != expected {
return fmt.Errorf("Policy: got: %#v want: %#v", mj.Policy, expected)
}
return nil
}
}
func testAccCheckMonitoringJobConfigSend(mj *monitor.Job, expected string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if mj.Config["send"].(string) != expected {
return fmt.Errorf("Config.send: got: %#v want: %#v", mj.Config["send"].(string), expected)
}
return nil
}
}
func testAccCheckMonitoringJobConfigPort(mj *monitor.Job, expected float64) resource.TestCheckFunc {
return func(s *terraform.State) error {
if mj.Config["port"].(float64) != expected {
return fmt.Errorf("Config.port: got: %#v want: %#v", mj.Config["port"].(float64), expected)
}
return nil
}
}
func testAccCheckMonitoringJobConfigHost(mj *monitor.Job, expected string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if mj.Config["host"].(string) != expected {
return fmt.Errorf("Config.host: got: %#v want: %#v", mj.Config["host"].(string), expected)
}
return nil
}
}
const testAccMonitoringJobBasic = `
resource "ns1_monitoringjob" "it" {
job_type = "tcp"
name = "terraform test"
regions = ["lga"]
frequency = 60
config {
send = "HEAD / HTTP/1.0\r\n\r\n"
port = 80
host = "1.1.1.1"
}
}
`
const testAccMonitoringJobUpdated = `
resource "ns1_monitoringjob" "it" {
job_type = "tcp"
name = "terraform test"
active = true
regions = ["lga"]
frequency = 120
rapid_recheck = true
policy = "all"
config {
send = "HEAD / HTTP/1.0\r\n\r\n"
port = 443
host = "1.1.1.1"
}
}
`

View File

@ -0,0 +1,367 @@
package ns1
import (
"errors"
"fmt"
"log"
"strconv"
"strings"
"github.com/hashicorp/terraform/helper/schema"
"github.com/mitchellh/hashstructure"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/data"
"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
"gopkg.in/ns1/ns1-go.v2/rest/model/filter"
)
var recordTypeStringEnum *StringEnum = NewStringEnum([]string{
"A",
"AAAA",
"ALIAS",
"AFSDB",
"CNAME",
"DNAME",
"HINFO",
"MX",
"NAPTR",
"NS",
"PTR",
"RP",
"SPF",
"SRV",
"TXT",
})
func recordResource() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
// Required
"zone": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"domain": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"type": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: recordTypeStringEnum.ValidateFunc,
},
// Optional
"ttl": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
},
// "meta": metaSchema,
"link": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"use_client_subnet": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"answers": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"answer": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"region": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
// "meta": metaSchema,
},
},
Set: genericHasher,
},
"regions": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
// "meta": metaSchema,
},
},
Set: genericHasher,
},
"filters": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"filter": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"disabled": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"config": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
},
},
},
},
// Computed
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
Create: RecordCreate,
Read: RecordRead,
Update: RecordUpdate,
Delete: RecordDelete,
Importer: &schema.ResourceImporter{State: RecordStateFunc},
}
}
func genericHasher(v interface{}) int {
hash, err := hashstructure.Hash(v, nil)
if err != nil {
panic(fmt.Sprintf("error computing hash code for %#v: %s", v, err.Error()))
}
return int(hash)
}
func recordToResourceData(d *schema.ResourceData, r *dns.Record) error {
d.SetId(r.ID)
d.Set("domain", r.Domain)
d.Set("zone", r.Zone)
d.Set("type", r.Type)
d.Set("ttl", r.TTL)
if r.Link != "" {
d.Set("link", r.Link)
}
// if r.Meta != nil {
// d.State()
// t := metaStructToDynamic(r.Meta)
// d.Set("meta", t)
// }
if len(r.Filters) > 0 {
filters := make([]map[string]interface{}, len(r.Filters))
for i, f := range r.Filters {
m := make(map[string]interface{})
m["filter"] = f.Type
if f.Disabled {
m["disabled"] = true
}
if f.Config != nil {
m["config"] = f.Config
}
filters[i] = m
}
d.Set("filters", filters)
}
if len(r.Answers) > 0 {
ans := &schema.Set{
F: genericHasher,
}
log.Printf("Got back from ns1 answers: %+v", r.Answers)
for _, answer := range r.Answers {
ans.Add(answerToMap(*answer))
}
log.Printf("Setting answers %+v", ans)
err := d.Set("answers", ans)
if err != nil {
return fmt.Errorf("[DEBUG] Error setting answers for: %s, error: %#v", r.Domain, err)
}
}
if len(r.Regions) > 0 {
regions := make([]map[string]interface{}, 0, len(r.Regions))
for regionName, _ := range r.Regions {
newRegion := make(map[string]interface{})
newRegion["name"] = regionName
// newRegion["meta"] = metaStructToDynamic(&region.Meta)
regions = append(regions, newRegion)
}
log.Printf("Setting regions %+v", regions)
err := d.Set("regions", regions)
if err != nil {
return fmt.Errorf("[DEBUG] Error setting regions for: %s, error: %#v", r.Domain, err)
}
}
return nil
}
func answerToMap(a dns.Answer) map[string]interface{} {
m := make(map[string]interface{})
m["answer"] = strings.Join(a.Rdata, " ")
if a.RegionName != "" {
m["region"] = a.RegionName
}
// if a.Meta != nil {
// m["meta"] = metaStructToDynamic(a.Meta)
// }
return m
}
func btoi(b bool) int {
if b {
return 1
}
return 0
}
func resourceDataToRecord(r *dns.Record, d *schema.ResourceData) error {
r.ID = d.Id()
if answers := d.Get("answers").(*schema.Set); answers.Len() > 0 {
al := make([]*dns.Answer, answers.Len())
for i, answerRaw := range answers.List() {
answer := answerRaw.(map[string]interface{})
var a *dns.Answer
v := answer["answer"].(string)
switch d.Get("type") {
case "TXT":
a = dns.NewTXTAnswer(v)
default:
a = dns.NewAnswer(strings.Split(v, " "))
}
if v, ok := answer["region"]; ok {
a.RegionName = v.(string)
}
// if v, ok := answer["meta"]; ok {
// metaDynamicToStruct(a.Meta, v)
// }
al[i] = a
}
r.Answers = al
if _, ok := d.GetOk("link"); ok {
return errors.New("Cannot have both link and answers in a record")
}
}
if v, ok := d.GetOk("ttl"); ok {
r.TTL = v.(int)
}
if v, ok := d.GetOk("link"); ok {
r.LinkTo(v.(string))
}
// if v, ok := d.GetOk("meta"); ok {
// metaDynamicToStruct(r.Meta, v)
// }
useClientSubnetVal := d.Get("use_client_subnet").(bool)
if v := strconv.FormatBool(useClientSubnetVal); v != "" {
r.UseClientSubnet = &useClientSubnetVal
}
if rawFilters := d.Get("filters").([]interface{}); len(rawFilters) > 0 {
f := make([]*filter.Filter, len(rawFilters))
for i, filterRaw := range rawFilters {
fi := filterRaw.(map[string]interface{})
config := make(map[string]interface{})
filter := filter.Filter{
Type: fi["filter"].(string),
Config: config,
}
if disabled, ok := fi["disabled"]; ok {
filter.Disabled = disabled.(bool)
}
if rawConfig, ok := fi["config"]; ok {
for k, v := range rawConfig.(map[string]interface{}) {
if i, err := strconv.Atoi(v.(string)); err == nil {
filter.Config[k] = i
} else {
filter.Config[k] = v
}
}
}
f[i] = &filter
}
r.Filters = f
}
if regions := d.Get("regions").(*schema.Set); regions.Len() > 0 {
for _, regionRaw := range regions.List() {
region := regionRaw.(map[string]interface{})
ns1R := data.Region{
Meta: data.Meta{},
}
// if v, ok := region["meta"]; ok {
// metaDynamicToStruct(&ns1R.Meta, v)
// }
r.Regions[region["name"].(string)] = ns1R
}
}
return nil
}
// RecordCreate creates DNS record in ns1
func RecordCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
r := dns.NewRecord(d.Get("zone").(string), d.Get("domain").(string), d.Get("type").(string))
if err := resourceDataToRecord(r, d); err != nil {
return err
}
if _, err := client.Records.Create(r); err != nil {
return err
}
return recordToResourceData(d, r)
}
// RecordRead reads the DNS record from ns1
func RecordRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
r, _, err := client.Records.Get(d.Get("zone").(string), d.Get("domain").(string), d.Get("type").(string))
if err != nil {
return err
}
return recordToResourceData(d, r)
}
// RecordDelete deltes the DNS record from ns1
func RecordDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
_, err := client.Records.Delete(d.Get("zone").(string), d.Get("domain").(string), d.Get("type").(string))
d.SetId("")
return err
}
// RecordUpdate updates the given dns record in ns1
func RecordUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
r := dns.NewRecord(d.Get("zone").(string), d.Get("domain").(string), d.Get("type").(string))
if err := resourceDataToRecord(r, d); err != nil {
return err
}
if _, err := client.Records.Update(r); err != nil {
return err
}
return recordToResourceData(d, r)
}
func RecordStateFunc(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
parts := strings.Split(d.Id(), "/")
if len(parts) != 3 {
return nil, fmt.Errorf("Invalid record specifier. Expecting 2 slashes (\"zone/domain/type\"), got %d.", len(parts)-1)
}
d.Set("zone", parts[0])
d.Set("domain", parts[1])
d.Set("type", parts[2])
return []*schema.ResourceData{d}, nil
}

View File

@ -0,0 +1,287 @@
package ns1
import (
"fmt"
"reflect"
"sort"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
)
func TestAccRecord_basic(t *testing.T) {
var record dns.Record
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckRecordDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccRecordBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckRecordExists("ns1_record.it", &record),
testAccCheckRecordDomain(&record, "test.terraform-record-test.io"),
testAccCheckRecordTTL(&record, 60),
testAccCheckRecordRegionName(&record, []string{"cal"}),
// testAccCheckRecordAnswerMetaWeight(&record, 10),
testAccCheckRecordAnswerRdata(&record, "test1.terraform-record-test.io"),
),
},
},
})
}
func TestAccRecord_updated(t *testing.T) {
var record dns.Record
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckRecordDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccRecordBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckRecordExists("ns1_record.it", &record),
testAccCheckRecordDomain(&record, "test.terraform-record-test.io"),
testAccCheckRecordTTL(&record, 60),
testAccCheckRecordRegionName(&record, []string{"cal"}),
// testAccCheckRecordAnswerMetaWeight(&record, 10),
testAccCheckRecordAnswerRdata(&record, "test1.terraform-record-test.io"),
),
},
resource.TestStep{
Config: testAccRecordUpdated,
Check: resource.ComposeTestCheckFunc(
testAccCheckRecordExists("ns1_record.it", &record),
testAccCheckRecordDomain(&record, "test.terraform-record-test.io"),
testAccCheckRecordTTL(&record, 120),
testAccCheckRecordRegionName(&record, []string{"ny", "wa"}),
// testAccCheckRecordAnswerMetaWeight(&record, 5),
testAccCheckRecordAnswerRdata(&record, "test2.terraform-record-test.io"),
),
},
},
})
}
func testAccCheckRecordExists(n string, record *dns.Record) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %v", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("NoID is set")
}
client := testAccProvider.Meta().(*ns1.Client)
p := rs.Primary
foundRecord, _, err := client.Records.Get(p.Attributes["zone"], p.Attributes["domain"], p.Attributes["type"])
if err != nil {
return fmt.Errorf("Record not found")
}
if foundRecord.Domain != p.Attributes["domain"] {
return fmt.Errorf("Record not found")
}
*record = *foundRecord
return nil
}
}
func testAccCheckRecordDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*ns1.Client)
var recordDomain string
var recordZone string
var recordType string
for _, rs := range s.RootModule().Resources {
if rs.Type != "ns1_record" {
continue
}
if rs.Type == "ns1_record" {
recordType = rs.Primary.Attributes["type"]
recordDomain = rs.Primary.Attributes["domain"]
recordZone = rs.Primary.Attributes["zone"]
}
}
foundRecord, _, err := client.Records.Get(recordDomain, recordZone, recordType)
if err != nil {
return fmt.Errorf("Record still exists: %#v", foundRecord)
}
return nil
}
func testAccCheckRecordDomain(r *dns.Record, expected string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if r.Domain != expected {
return fmt.Errorf("Domain: got: %#v want: %#v", r.Domain, expected)
}
return nil
}
}
func testAccCheckRecordTTL(r *dns.Record, expected int) resource.TestCheckFunc {
return func(s *terraform.State) error {
if r.TTL != expected {
return fmt.Errorf("TTL: got: %#v want: %#v", r.TTL, expected)
}
return nil
}
}
func testAccCheckRecordRegionName(r *dns.Record, expected []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
regions := make([]string, len(r.Regions))
i := 0
for k := range r.Regions {
regions[i] = k
i++
}
sort.Strings(regions)
sort.Strings(expected)
if !reflect.DeepEqual(regions, expected) {
return fmt.Errorf("Regions: got: %#v want: %#v", regions, expected)
}
return nil
}
}
func testAccCheckRecordAnswerMetaWeight(r *dns.Record, expected float64) resource.TestCheckFunc {
return func(s *terraform.State) error {
recordAnswer := r.Answers[0]
recordMetas := recordAnswer.Meta
weight := recordMetas.Weight.(float64)
if weight != expected {
return fmt.Errorf("Answers[0].Meta.Weight: got: %#v want: %#v", weight, expected)
}
return nil
}
}
func testAccCheckRecordAnswerRdata(r *dns.Record, expected string) resource.TestCheckFunc {
return func(s *terraform.State) error {
recordAnswer := r.Answers[0]
recordAnswerString := recordAnswer.Rdata[0]
if recordAnswerString != expected {
return fmt.Errorf("Answers[0].Rdata[0]: got: %#v want: %#v", recordAnswerString, expected)
}
return nil
}
}
const testAccRecordBasic = `
resource "ns1_record" "it" {
zone = "${ns1_zone.test.zone}"
domain = "test.${ns1_zone.test.zone}"
type = "CNAME"
ttl = 60
// meta {
// weight = 5
// connections = 3
// // up = false // Ignored by d.GetOk("meta.0.up") due to known issue
// }
answers {
answer = "test1.terraform-record-test.io"
region = "cal"
// meta {
// weight = 10
// up = true
// }
}
regions {
name = "cal"
// meta {
// up = true
// us_state = ["CA"]
// }
}
filters {
filter = "up"
}
filters {
filter = "geotarget_country"
}
filters {
filter = "select_first_n"
config = {N=1}
}
}
resource "ns1_zone" "test" {
zone = "terraform-record-test.io"
}
`
const testAccRecordUpdated = `
resource "ns1_record" "it" {
zone = "${ns1_zone.test.zone}"
domain = "test.${ns1_zone.test.zone}"
type = "CNAME"
ttl = 120
use_client_subnet = true
// meta {
// weight = 5
// connections = 3
// // up = false // Ignored by d.GetOk("meta.0.up") due to known issue
// }
answers {
answer = "test2.terraform-record-test.io"
region = "ny"
// meta {
// weight = 5
// up = true
// }
}
regions {
name = "wa"
// meta {
// us_state = ["WA"]
// }
}
regions {
name = "ny"
// meta {
// us_state = ["NY"]
// }
}
filters {
filter = "up"
}
filters {
filter = "geotarget_country"
}
}
resource "ns1_zone" "test" {
zone = "terraform-record-test.io"
}
`

View File

@ -0,0 +1,89 @@
package ns1
import (
"github.com/hashicorp/terraform/helper/schema"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/account"
)
func teamResource() *schema.Resource {
s := map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
}
s = addPermsSchema(s)
return &schema.Resource{
Schema: s,
Create: TeamCreate,
Read: TeamRead,
Update: TeamUpdate,
Delete: TeamDelete,
}
}
func teamToResourceData(d *schema.ResourceData, t *account.Team) error {
d.SetId(t.ID)
d.Set("name", t.Name)
permissionsToResourceData(d, t.Permissions)
return nil
}
func resourceDataToTeam(t *account.Team, d *schema.ResourceData) error {
t.ID = d.Id()
t.Name = d.Get("name").(string)
t.Permissions = resourceDataToPermissions(d)
return nil
}
// TeamCreate creates the given team in ns1
func TeamCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
t := account.Team{}
if err := resourceDataToTeam(&t, d); err != nil {
return err
}
if _, err := client.Teams.Create(&t); err != nil {
return err
}
return teamToResourceData(d, &t)
}
// TeamRead reads the team data from ns1
func TeamRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
t, _, err := client.Teams.Get(d.Id())
if err != nil {
return err
}
return teamToResourceData(d, t)
}
// TeamDelete deletes the given team from ns1
func TeamDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
_, err := client.Teams.Delete(d.Id())
d.SetId("")
return err
}
// TeamUpdate updates the given team in ns1
func TeamUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
t := account.Team{
ID: d.Id(),
}
if err := resourceDataToTeam(&t, d); err != nil {
return err
}
if _, err := client.Teams.Update(&t); err != nil {
return err
}
return teamToResourceData(d, &t)
}

View File

@ -0,0 +1,209 @@
package ns1
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/account"
)
func TestAccTeam_basic(t *testing.T) {
var team account.Team
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckTeamDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccTeamBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckTeamExists("ns1_team.foobar", &team),
testAccCheckTeamName(&team, "terraform test"),
testAccCheckTeamDNSPermission(&team, "view_zones", true),
testAccCheckTeamDNSPermission(&team, "zones_allow_by_default", true),
testAccCheckTeamDNSPermissionZones(&team, "zones_allow", []string{"mytest.zone"}),
testAccCheckTeamDNSPermissionZones(&team, "zones_deny", []string{"myother.zone"}),
testAccCheckTeamDataPermission(&team, "manage_datasources", true),
),
},
},
})
}
func TestAccTeam_updated(t *testing.T) {
var team account.Team
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckTeamDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccTeamBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckTeamExists("ns1_team.foobar", &team),
testAccCheckTeamName(&team, "terraform test"),
),
},
resource.TestStep{
Config: testAccTeamUpdated,
Check: resource.ComposeTestCheckFunc(
testAccCheckTeamExists("ns1_team.foobar", &team),
testAccCheckTeamName(&team, "terraform test updated"),
testAccCheckTeamDNSPermission(&team, "view_zones", true),
testAccCheckTeamDNSPermission(&team, "zones_allow_by_default", true),
testAccCheckTeamDNSPermissionZones(&team, "zones_allow", []string{}),
testAccCheckTeamDNSPermissionZones(&team, "zones_deny", []string{}),
testAccCheckTeamDataPermission(&team, "manage_datasources", false),
),
},
},
})
}
func testAccCheckTeamExists(n string, team *account.Team) 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("NoID is set")
}
client := testAccProvider.Meta().(*ns1.Client)
foundTeam, _, err := client.Teams.Get(rs.Primary.Attributes["id"])
if err != nil {
return err
}
if foundTeam.Name != rs.Primary.Attributes["name"] {
return fmt.Errorf("Team not found")
}
*team = *foundTeam
return nil
}
}
func testAccCheckTeamDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*ns1.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "ns1_team" {
continue
}
team, _, err := client.Teams.Get(rs.Primary.Attributes["id"])
if err == nil {
return fmt.Errorf("Team still exists: %#v: %#v", err, team.Name)
}
}
return nil
}
func testAccCheckTeamName(team *account.Team, expected string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if team.Name != expected {
return fmt.Errorf("Name: got: %s want: %s", team.Name, expected)
}
return nil
}
}
func testAccCheckTeamDNSPermission(team *account.Team, perm string, expected bool) resource.TestCheckFunc {
return func(s *terraform.State) error {
dns := team.Permissions.DNS
switch perm {
case "view_zones":
if dns.ViewZones != expected {
return fmt.Errorf("DNS.ViewZones: got: %t want: %t", dns.ViewZones, expected)
}
case "manage_zones":
if dns.ManageZones != expected {
return fmt.Errorf("DNS.ManageZones: got: %t want: %t", dns.ManageZones, expected)
}
case "zones_allow_by_default":
if dns.ZonesAllowByDefault != expected {
return fmt.Errorf("DNS.ZonesAllowByDefault: got: %t want: %t", dns.ZonesAllowByDefault, expected)
}
}
return nil
}
}
func testAccCheckTeamDataPermission(team *account.Team, perm string, expected bool) resource.TestCheckFunc {
return func(s *terraform.State) error {
data := team.Permissions.Data
switch perm {
case "push_to_datafeeds":
if data.PushToDatafeeds != expected {
return fmt.Errorf("Data.PushToDatafeeds: got: %t want: %t", data.PushToDatafeeds, expected)
}
case "manage_datasources":
if data.ManageDatasources != expected {
return fmt.Errorf("Data.ManageDatasources: got: %t want: %t", data.ManageDatasources, expected)
}
case "manage_datafeeds":
if data.ManageDatafeeds != expected {
return fmt.Errorf("Data.ManageDatafeeds: got: %t want: %t", data.ManageDatafeeds, expected)
}
}
return nil
}
}
func testAccCheckTeamDNSPermissionZones(team *account.Team, perm string, expected []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
dns := team.Permissions.DNS
switch perm {
case "zones_allow":
if !reflect.DeepEqual(dns.ZonesAllow, expected) {
return fmt.Errorf("DNS.ZonesAllow: got: %v want: %v", dns.ZonesAllow, expected)
}
case "zones_deny":
if !reflect.DeepEqual(dns.ZonesDeny, expected) {
return fmt.Errorf("DNS.ZonesDeny: got: %v want: %v", dns.ZonesDeny, expected)
}
}
return nil
}
}
const testAccTeamBasic = `
resource "ns1_team" "foobar" {
name = "terraform test"
dns_view_zones = true
dns_zones_allow_by_default = true
dns_zones_allow = ["mytest.zone"]
dns_zones_deny = ["myother.zone"]
data_manage_datasources = true
}`
const testAccTeamUpdated = `
resource "ns1_team" "foobar" {
name = "terraform test updated"
dns_view_zones = true
dns_zones_allow_by_default = true
data_manage_datasources = false
}`

View File

@ -0,0 +1,133 @@
package ns1
import (
"github.com/hashicorp/terraform/helper/schema"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/account"
)
func userResource() *schema.Resource {
s := map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"username": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"email": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"notify": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"billing": &schema.Schema{
Type: schema.TypeBool,
Required: true,
},
},
},
},
"teams": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
}
s = addPermsSchema(s)
return &schema.Resource{
Schema: s,
Create: UserCreate,
Read: UserRead,
Update: UserUpdate,
Delete: UserDelete,
}
}
func userToResourceData(d *schema.ResourceData, u *account.User) error {
d.SetId(u.Username)
d.Set("name", u.Name)
d.Set("email", u.Email)
d.Set("teams", u.TeamIDs)
notify := make(map[string]bool)
notify["billing"] = u.Notify.Billing
d.Set("notify", notify)
permissionsToResourceData(d, u.Permissions)
return nil
}
func resourceDataToUser(u *account.User, d *schema.ResourceData) error {
u.Name = d.Get("name").(string)
u.Username = d.Get("username").(string)
u.Email = d.Get("email").(string)
if v, ok := d.GetOk("teams"); ok {
teamsRaw := v.([]interface{})
u.TeamIDs = make([]string, len(teamsRaw))
for i, team := range teamsRaw {
u.TeamIDs[i] = team.(string)
}
} else {
u.TeamIDs = make([]string, 0)
}
if v, ok := d.GetOk("notify"); ok {
notifyRaw := v.(map[string]interface{})
u.Notify.Billing = notifyRaw["billing"].(bool)
}
u.Permissions = resourceDataToPermissions(d)
return nil
}
// UserCreate creates the given user in ns1
func UserCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
u := account.User{}
if err := resourceDataToUser(&u, d); err != nil {
return err
}
if _, err := client.Users.Create(&u); err != nil {
return err
}
return userToResourceData(d, &u)
}
// UserRead reads the given users data from ns1
func UserRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
u, _, err := client.Users.Get(d.Id())
if err != nil {
return err
}
return userToResourceData(d, u)
}
// UserDelete deletes the given user from ns1
func UserDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
_, err := client.Users.Delete(d.Id())
d.SetId("")
return err
}
// UserUpdate updates the user with given parameters in ns1
func UserUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
u := account.User{
Username: d.Id(),
}
if err := resourceDataToUser(&u, d); err != nil {
return err
}
if _, err := client.Users.Update(&u); err != nil {
return err
}
return userToResourceData(d, &u)
}

View File

@ -0,0 +1,174 @@
package ns1
import (
"strings"
"github.com/hashicorp/terraform/helper/schema"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
)
func zoneResource() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
// Required
"zone": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
// Optional
"ttl": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
},
// SOA attributes per https://tools.ietf.org/html/rfc1035).
"refresh": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
},
"retry": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
},
"expiry": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
},
// SOA MINUMUM overloaded as NX TTL per https://tools.ietf.org/html/rfc2308
"nx_ttl": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
},
// TODO: test
"link": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
// TODO: test
"primary": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
// Computed
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"dns_servers": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"hostmaster": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
Create: ZoneCreate,
Read: ZoneRead,
Update: ZoneUpdate,
Delete: ZoneDelete,
Importer: &schema.ResourceImporter{State: ZoneStateFunc},
}
}
func zoneToResourceData(d *schema.ResourceData, z *dns.Zone) {
d.SetId(z.ID)
d.Set("hostmaster", z.Hostmaster)
d.Set("ttl", z.TTL)
d.Set("nx_ttl", z.NxTTL)
d.Set("refresh", z.Refresh)
d.Set("retry", z.Retry)
d.Set("expiry", z.Expiry)
d.Set("dns_servers", strings.Join(z.DNSServers[:], ","))
if z.Secondary != nil && z.Secondary.Enabled {
d.Set("primary", z.Secondary.PrimaryIP)
}
if z.Link != nil && *z.Link != "" {
d.Set("link", *z.Link)
}
}
func resourceToZoneData(z *dns.Zone, d *schema.ResourceData) {
z.ID = d.Id()
if v, ok := d.GetOk("hostmaster"); ok {
z.Hostmaster = v.(string)
}
if v, ok := d.GetOk("ttl"); ok {
z.TTL = v.(int)
}
if v, ok := d.GetOk("nx_ttl"); ok {
z.NxTTL = v.(int)
}
if v, ok := d.GetOk("refresh"); ok {
z.Refresh = v.(int)
}
if v, ok := d.GetOk("retry"); ok {
z.Retry = v.(int)
}
if v, ok := d.GetOk("expiry"); ok {
z.Expiry = v.(int)
}
if v, ok := d.GetOk("primary"); ok {
z.MakeSecondary(v.(string))
}
if v, ok := d.GetOk("link"); ok {
z.LinkTo(v.(string))
}
}
// ZoneCreate creates the given zone in ns1
func ZoneCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
z := dns.NewZone(d.Get("zone").(string))
resourceToZoneData(z, d)
if _, err := client.Zones.Create(z); err != nil {
return err
}
zoneToResourceData(d, z)
return nil
}
// ZoneRead reads the given zone data from ns1
func ZoneRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
z, _, err := client.Zones.Get(d.Get("zone").(string))
if err != nil {
return err
}
zoneToResourceData(d, z)
return nil
}
// ZoneDelete deteles the given zone from ns1
func ZoneDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
_, err := client.Zones.Delete(d.Get("zone").(string))
d.SetId("")
return err
}
// ZoneUpdate updates the zone with given params in ns1
func ZoneUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ns1.Client)
z := dns.NewZone(d.Get("zone").(string))
resourceToZoneData(z, d)
if _, err := client.Zones.Update(z); err != nil {
return err
}
zoneToResourceData(d, z)
return nil
}
func ZoneStateFunc(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
d.Set("zone", d.Id())
return []*schema.ResourceData{d}, nil
}

View File

@ -0,0 +1,189 @@
package ns1
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
ns1 "gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
)
func TestAccZone_basic(t *testing.T) {
var zone dns.Zone
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckZoneDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccZoneBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckZoneExists("ns1_zone.it", &zone),
testAccCheckZoneName(&zone, "terraform-test-zone.io"),
testAccCheckZoneTTL(&zone, 3600),
testAccCheckZoneRefresh(&zone, 43200),
testAccCheckZoneRetry(&zone, 7200),
testAccCheckZoneExpiry(&zone, 1209600),
testAccCheckZoneNxTTL(&zone, 3600),
),
},
},
})
}
func TestAccZone_updated(t *testing.T) {
var zone dns.Zone
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckZoneDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccZoneBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckZoneExists("ns1_zone.it", &zone),
testAccCheckZoneName(&zone, "terraform-test-zone.io"),
testAccCheckZoneTTL(&zone, 3600),
testAccCheckZoneRefresh(&zone, 43200),
testAccCheckZoneRetry(&zone, 7200),
testAccCheckZoneExpiry(&zone, 1209600),
testAccCheckZoneNxTTL(&zone, 3600),
),
},
resource.TestStep{
Config: testAccZoneUpdated,
Check: resource.ComposeTestCheckFunc(
testAccCheckZoneExists("ns1_zone.it", &zone),
testAccCheckZoneName(&zone, "terraform-test-zone.io"),
testAccCheckZoneTTL(&zone, 10800),
testAccCheckZoneRefresh(&zone, 3600),
testAccCheckZoneRetry(&zone, 300),
testAccCheckZoneExpiry(&zone, 2592000),
testAccCheckZoneNxTTL(&zone, 3601),
),
},
},
})
}
func testAccCheckZoneExists(n string, zone *dns.Zone) 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("NoID is set")
}
client := testAccProvider.Meta().(*ns1.Client)
foundZone, _, err := client.Zones.Get(rs.Primary.Attributes["zone"])
p := rs.Primary
if err != nil {
return err
}
if foundZone.ID != p.Attributes["id"] {
return fmt.Errorf("Zone not found")
}
*zone = *foundZone
return nil
}
}
func testAccCheckZoneDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*ns1.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "ns1_zone" {
continue
}
zone, _, err := client.Zones.Get(rs.Primary.Attributes["zone"])
if err == nil {
return fmt.Errorf("Zone still exists: %#v: %#v", err, zone)
}
}
return nil
}
func testAccCheckZoneName(zone *dns.Zone, expected string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if zone.Zone != expected {
return fmt.Errorf("Zone: got: %s want: %s", zone.Zone, expected)
}
return nil
}
}
func testAccCheckZoneTTL(zone *dns.Zone, expected int) resource.TestCheckFunc {
return func(s *terraform.State) error {
if zone.TTL != expected {
return fmt.Errorf("TTL: got: %d want: %d", zone.TTL, expected)
}
return nil
}
}
func testAccCheckZoneRefresh(zone *dns.Zone, expected int) resource.TestCheckFunc {
return func(s *terraform.State) error {
if zone.Refresh != expected {
return fmt.Errorf("Refresh: got: %d want: %d", zone.Refresh, expected)
}
return nil
}
}
func testAccCheckZoneRetry(zone *dns.Zone, expected int) resource.TestCheckFunc {
return func(s *terraform.State) error {
if zone.Retry != expected {
return fmt.Errorf("Retry: got: %d want: %d", zone.Retry, expected)
}
return nil
}
}
func testAccCheckZoneExpiry(zone *dns.Zone, expected int) resource.TestCheckFunc {
return func(s *terraform.State) error {
if zone.Expiry != expected {
return fmt.Errorf("Expiry: got: %d want: %d", zone.Expiry, expected)
}
return nil
}
}
func testAccCheckZoneNxTTL(zone *dns.Zone, expected int) resource.TestCheckFunc {
return func(s *terraform.State) error {
if zone.NxTTL != expected {
return fmt.Errorf("NxTTL: got: %d want: %d", zone.NxTTL, expected)
}
return nil
}
}
const testAccZoneBasic = `
resource "ns1_zone" "it" {
zone = "terraform-test-zone.io"
}
`
const testAccZoneUpdated = `
resource "ns1_zone" "it" {
zone = "terraform-test-zone.io"
ttl = 10800
refresh = 3600
retry = 300
expiry = 2592000
nx_ttl = 3601
# link = "1.2.3.4.in-addr.arpa" # TODO
# primary = "1.2.3.4.in-addr.arpa" # TODO
}
`

View File

@ -0,0 +1,47 @@
package ns1
import (
"fmt"
"strings"
)
type StringEnum struct {
ValueMap map[string]int
Expecting string
}
func NewStringEnum(values []string) *StringEnum {
valueMap := make(map[string]int)
quoted := make([]string, len(values), len(values))
for i, value := range values {
_, present := valueMap[value]
if present {
panic(fmt.Sprintf("duplicate value %q", value))
}
valueMap[value] = i
quoted[i] = fmt.Sprintf("%q", value)
}
return &StringEnum{
ValueMap: valueMap,
Expecting: strings.Join(quoted, ", "),
}
}
func (se *StringEnum) Check(v string) (int, error) {
i, present := se.ValueMap[v]
if present {
return i, nil
} else {
return -1, fmt.Errorf("expecting one of %s; got %q", se.Expecting, v)
}
}
func (se *StringEnum) ValidateFunc(v interface{}, k string) (ws []string, es []error) {
_, err := se.Check(v.(string))
if err != nil {
return nil, []error{err}
}
return nil, nil
}

View File

@ -78,6 +78,19 @@ func resourceComputeVolumeAttachV2Create(d *schema.ResourceData, meta interface{
return err
}
stateConf := &resource.StateChangeConf{
Pending: []string{"ATTACHING"},
Target: []string{"ATTACHED"},
Refresh: resourceComputeVolumeAttachV2AttachFunc(computeClient, instanceId, attachment.ID),
Timeout: 10 * time.Minute,
Delay: 30 * time.Second,
MinTimeout: 15 * time.Second,
}
if _, err = stateConf.WaitForState(); err != nil {
return fmt.Errorf("Error attaching OpenStack volume: %s", err)
}
log.Printf("[DEBUG] Created volume attachment: %#v", attachment)
// Use the instance ID and attachment ID as the resource ID.
@ -131,7 +144,7 @@ func resourceComputeVolumeAttachV2Delete(d *schema.ResourceData, meta interface{
stateConf := &resource.StateChangeConf{
Pending: []string{""},
Target: []string{"DETACHED"},
Refresh: volumeDetachRefreshFunc(computeClient, instanceId, attachmentId),
Refresh: resourceComputeVolumeAttachV2DetachFunc(computeClient, instanceId, attachmentId),
Timeout: 10 * time.Minute,
Delay: 15 * time.Second,
MinTimeout: 15 * time.Second,
@ -144,9 +157,26 @@ func resourceComputeVolumeAttachV2Delete(d *schema.ResourceData, meta interface{
return nil
}
func volumeDetachRefreshFunc(computeClient *gophercloud.ServiceClient, instanceId, attachmentId string) resource.StateRefreshFunc {
func resourceComputeVolumeAttachV2AttachFunc(
computeClient *gophercloud.ServiceClient, instanceId, attachmentId string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
log.Printf("[DEBUG] Attempting to detach OpenStack volume %s from instance %s", attachmentId, instanceId)
va, err := volumeattach.Get(computeClient, instanceId, attachmentId).Extract()
if err != nil {
if _, ok := err.(gophercloud.ErrDefault404); ok {
return va, "ATTACHING", nil
}
return va, "", err
}
return va, "ATTACHED", nil
}
}
func resourceComputeVolumeAttachV2DetachFunc(
computeClient *gophercloud.ServiceClient, instanceId, attachmentId string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
log.Printf("[DEBUG] Attempting to detach OpenStack volume %s from instance %s",
attachmentId, instanceId)
va, err := volumeattach.Get(computeClient, instanceId, attachmentId).Extract()
if err != nil {

View File

@ -28,6 +28,25 @@ func TestAccComputeV2VolumeAttach_basic(t *testing.T) {
})
}
func TestAccComputeV2VolumeAttach_device(t *testing.T) {
var va volumeattach.VolumeAttachment
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckComputeV2VolumeAttachDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccComputeV2VolumeAttach_device,
Check: resource.ComposeTestCheckFunc(
testAccCheckComputeV2VolumeAttachExists("openstack_compute_volume_attach_v2.va_1", &va),
testAccCheckComputeV2VolumeAttachDevice(&va, "/dev/vdc"),
),
},
},
})
}
func testAccCheckComputeV2VolumeAttachDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
computeClient, err := config.computeV2Client(OS_REGION_NAME)
@ -91,6 +110,18 @@ func testAccCheckComputeV2VolumeAttachExists(n string, va *volumeattach.VolumeAt
}
}
func testAccCheckComputeV2VolumeAttachDevice(
va *volumeattach.VolumeAttachment, device string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if va.Device != device {
return fmt.Errorf("Requested device of volume attachment (%s) does not match: %s",
device, va.Device)
}
return nil
}
}
const testAccComputeV2VolumeAttach_basic = `
resource "openstack_blockstorage_volume_v2" "volume_1" {
name = "volume_1"
@ -107,3 +138,21 @@ resource "openstack_compute_volume_attach_v2" "va_1" {
volume_id = "${openstack_blockstorage_volume_v2.volume_1.id}"
}
`
const testAccComputeV2VolumeAttach_device = `
resource "openstack_blockstorage_volume_v2" "volume_1" {
name = "volume_1"
size = 1
}
resource "openstack_compute_instance_v2" "instance_1" {
name = "instance_1"
security_groups = ["default"]
}
resource "openstack_compute_volume_attach_v2" "va_1" {
instance_id = "${openstack_compute_instance_v2.instance_1.id}"
volume_id = "${openstack_blockstorage_volume_v2.volume_1.id}"
device = "/dev/vdc"
}
`

View File

@ -28,7 +28,7 @@ func TestAccPostgresqlRole_Basic(t *testing.T) {
resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "bypass_row_level_security", "false"),
resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "connection_limit", "-1"),
resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "encrypted_password", "true"),
resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "password", ""),
resource.TestCheckNoResourceAttr("postgresql_role.role_with_defaults", "password"),
resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "valid_until", "infinity"),
resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "skip_drop_role", "false"),
resource.TestCheckResourceAttr("postgresql_role.role_with_defaults", "skip_reassign_owned", "false"),

View File

@ -144,6 +144,15 @@ Options:
-state-out=path Path to write updated state file. By default, the
"-state" path will be used.
-var 'foo=bar' Set a variable in the Terraform configuration. This
flag can be set multiple times. This is only useful
with the "-config" flag.
-var-file=foo Set variables in the Terraform configuration from
a file. If "terraform.tfvars" is present, it will be
automatically loaded if this flag is not specified.
`
return strings.TrimSpace(helpText)
}

View File

@ -159,6 +159,176 @@ func TestImport_providerConfigDisable(t *testing.T) {
testStateOutput(t, statePath, testImportStr)
}
func TestImport_providerConfigWithVar(t *testing.T) {
defer testChdir(t, testFixturePath("import-provider-var"))()
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.ImportStateFn = nil
p.ImportStateReturn = []*terraform.InstanceState{
&terraform.InstanceState{
ID: "yay",
Ephemeral: terraform.EphemeralState{
Type: "test_instance",
},
},
}
configured := false
p.ConfigureFn = func(c *terraform.ResourceConfig) error {
configured = true
if v, ok := c.Get("foo"); !ok || v.(string) != "bar" {
return fmt.Errorf("bad value: %#v", v)
}
return nil
}
args := []string{
"-state", statePath,
"-var", "foo=bar",
"test_instance.foo",
"bar",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Verify that we were called
if !configured {
t.Fatal("Configure should be called")
}
if !p.ImportStateCalled {
t.Fatal("ImportState should be called")
}
testStateOutput(t, statePath, testImportStr)
}
func TestImport_providerConfigWithVarDefault(t *testing.T) {
defer testChdir(t, testFixturePath("import-provider-var-default"))()
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.ImportStateFn = nil
p.ImportStateReturn = []*terraform.InstanceState{
&terraform.InstanceState{
ID: "yay",
Ephemeral: terraform.EphemeralState{
Type: "test_instance",
},
},
}
configured := false
p.ConfigureFn = func(c *terraform.ResourceConfig) error {
configured = true
if v, ok := c.Get("foo"); !ok || v.(string) != "bar" {
return fmt.Errorf("bad value: %#v", v)
}
return nil
}
args := []string{
"-state", statePath,
"test_instance.foo",
"bar",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Verify that we were called
if !configured {
t.Fatal("Configure should be called")
}
if !p.ImportStateCalled {
t.Fatal("ImportState should be called")
}
testStateOutput(t, statePath, testImportStr)
}
func TestImport_providerConfigWithVarFile(t *testing.T) {
defer testChdir(t, testFixturePath("import-provider-var-file"))()
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.ImportStateFn = nil
p.ImportStateReturn = []*terraform.InstanceState{
&terraform.InstanceState{
ID: "yay",
Ephemeral: terraform.EphemeralState{
Type: "test_instance",
},
},
}
configured := false
p.ConfigureFn = func(c *terraform.ResourceConfig) error {
configured = true
if v, ok := c.Get("foo"); !ok || v.(string) != "bar" {
return fmt.Errorf("bad value: %#v", v)
}
return nil
}
args := []string{
"-state", statePath,
"-var-file", "blah.tfvars",
"test_instance.foo",
"bar",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Verify that we were called
if !configured {
t.Fatal("Configure should be called")
}
if !p.ImportStateCalled {
t.Fatal("ImportState should be called")
}
testStateOutput(t, statePath, testImportStr)
}
/*
func TestRefresh_badState(t *testing.T) {
p := testProvider()

View File

@ -40,6 +40,7 @@ import (
mysqlprovider "github.com/hashicorp/terraform/builtin/providers/mysql"
newrelicprovider "github.com/hashicorp/terraform/builtin/providers/newrelic"
nomadprovider "github.com/hashicorp/terraform/builtin/providers/nomad"
ns1provider "github.com/hashicorp/terraform/builtin/providers/ns1"
nullprovider "github.com/hashicorp/terraform/builtin/providers/null"
openstackprovider "github.com/hashicorp/terraform/builtin/providers/openstack"
opsgenieprovider "github.com/hashicorp/terraform/builtin/providers/opsgenie"
@ -108,6 +109,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{
"mysql": mysqlprovider.Provider,
"newrelic": newrelicprovider.Provider,
"nomad": nomadprovider.Provider,
"ns1": ns1provider.Provider,
"null": nullprovider.Provider,
"openstack": openstackprovider.Provider,
"opsgenie": opsgenieprovider.Provider,

Some files were not shown because too many files have changed in this diff Show More