Merge branch 'master' into paddy_10984_better_image_resolution
This commit is contained in:
commit
95e01ad35b
|
@ -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:
|
||||
|
|
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package main
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -92,6 +92,7 @@ type Config struct {
|
|||
Insecure bool
|
||||
|
||||
SkipCredsValidation bool
|
||||
SkipRegionValidation bool
|
||||
SkipRequestingAccountId bool
|
||||
SkipMetadataApiCheck bool
|
||||
S3ForcePathStyle bool
|
||||
|
@ -153,11 +154,15 @@ 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.
|
||||
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
|
||||
// store AWS region in client struct, for region specific operations such as
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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}"]
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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" { }
|
||||
`
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -189,5 +189,9 @@ data "aws_route_table" "by_filter" {
|
|||
name = "association.main"
|
||||
values = ["true"]
|
||||
}
|
||||
filter {
|
||||
name = "vpc-id"
|
||||
values = ["vpc-6bd70802"]
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"]
|
||||
}
|
||||
`
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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}"]
|
||||
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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}"]
|
||||
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
},
|
||||
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
},
|
||||
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
},
|
||||
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
},
|
||||
|
||||
|
|
|
@ -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(¶ms); 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(¶ms)
|
||||
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -66,6 +66,7 @@ func resourceAwsDbInstance() *schema.Resource {
|
|||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
DiffSuppressFunc: suppressAwsDbEngineVersionDiffs,
|
||||
},
|
||||
|
||||
"character_set_name": {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}"]
|
||||
}
|
||||
|
||||
|
|
|
@ -212,7 +212,6 @@ func resourceAwsElasticTranscoderPreset() *schema.Resource {
|
|||
},
|
||||
"max_frame_rate": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Default: "30",
|
||||
Optional: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
},
|
||||
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,27 +23,64 @@ 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,
|
||||
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.",
|
||||
},
|
||||
"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,
|
||||
},
|
||||
"name": &schema.Schema{
|
||||
"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,
|
||||
|
@ -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)
|
||||
var pid string
|
||||
var err error
|
||||
pid = d.Get("project_id").(string)
|
||||
if pid == "" {
|
||||
pid, err = getProject(d, config)
|
||||
if err != nil {
|
||||
return err
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"),
|
||||
),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)}))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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",
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}`
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}`
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
`
|
|
@ -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(®ion.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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
`
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}`
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
`
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue