diff --git a/fivetran/data_source_destination.go b/fivetran/data_source_destination.go index ad6a144c..ef669b27 100644 --- a/fivetran/data_source_destination.go +++ b/fivetran/data_source_destination.go @@ -1,329 +1,12 @@ package fivetran import ( - "context" - "fmt" - "strconv" - - "github.com/fivetran/go-fivetran" - "github.com/fivetran/go-fivetran/destinations" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceDestination() *schema.Resource { return &schema.Resource{ - ReadContext: dataSourceDestinationRead, - Schema: map[string]*schema.Schema{ - "id": { - Type: schema.TypeString, - Required: true, - Description: "The unique identifier for the destination within the Fivetran system", - }, - "group_id": { - Type: schema.TypeString, - Computed: true, - Description: "The unique identifier for the Group within the Fivetran system.", - }, - "service": { - Type: schema.TypeString, - Computed: true, - Description: "The destination type name within the Fivetran system", - }, - "region": { - Type: schema.TypeString, - Computed: true, - Description: "Data processing location. This is where Fivetran will operate and run computation on data.", - }, - "time_zone_offset": { - Type: schema.TypeString, - Computed: true, - Description: "Determines the time zone for the Fivetran sync schedule.", - }, - "config": dataSourceDestinationSchemaConfig(), - "setup_status": { - Type: schema.TypeString, - Computed: true, - Description: "Destination setup status", - }, - }, - } -} - -func dataSourceDestinationSchemaConfig() *schema.Schema { - return &schema.Schema{ - Type: schema.TypeSet, - Optional: true, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "host": { - Type: schema.TypeString, - Optional: true, - Description: "Server name", - }, - "port": { - Type: schema.TypeInt, - Optional: true, - Description: "Server port number", - }, - "database": { - Type: schema.TypeString, - Optional: true, - Description: "Database name", - }, - "auth": { - Type: schema.TypeString, - Optional: true, - Description: "The connector authorization settings. Check possible config formats in [create method](/openapi/reference/v1/operation/create_connector/)", - }, - "user": { - Type: schema.TypeString, - Optional: true, - Description: "Database user name", - }, - "password": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - Description: "Database user password", - }, - "connection_type": { - Type: schema.TypeString, - Optional: true, - Description: "Connection method. Default value: `Directly`.", - }, - "tunnel_host": { - Type: schema.TypeString, - Optional: true, - Description: "SSH server name. Must be populated if `connection_type` is set to `SshTunnel`.", - }, - "tunnel_port": { - Type: schema.TypeString, - Optional: true, - Description: "SSH server port name. Must be populated if `connection_type` is set to `SshTunnel`.", - }, - "tunnel_user": { - Type: schema.TypeString, - Optional: true, - Description: "SSH user name. Must be populated if `connection_type` is set to `SshTunnel`.", - }, - "project_id": { - Type: schema.TypeString, - Optional: true, - Description: "BigQuery project ID", - }, - "data_set_location": { - Type: schema.TypeString, - Optional: true, - Description: "Data location. Datasets will reside in this location.", - }, - "bucket": { - Type: schema.TypeString, - Optional: true, - Description: "Customer bucket. If specified, your GCS bucket will be used to process the data instead of a Fivetran-managed bucket. The bucket must be present in the same location as the dataset location.", - }, - "server_host_name": { - Type: schema.TypeString, - Optional: true, - Description: "Server name", - }, - "http_path": { - Type: schema.TypeString, - Optional: true, - Description: "HTTP path", - }, - "personal_access_token": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - Description: "Personal access token", - }, - "create_external_tables": { - Type: schema.TypeString, - Optional: true, - Description: "Whether to create external tables", - }, - "external_location": { - Type: schema.TypeString, - Optional: true, - Description: "External location to store Delta tables. Default value: `\"\"` (null). By default, the external tables will reside in the `/{schema}/{table}` path, and if you specify an external location in the `{externalLocation}/{schema}/{table}` path.", - }, - "auth_type": { - Type: schema.TypeString, - Optional: true, - Description: "Authentication type. Default value: `PASSWORD`.", - }, - "role_arn": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - Description: "Role ARN with Redshift permissions. Required if authentication type is `IAM`.", - }, - "secret_key": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - Description: "Private key of the customer service account. If specified, your service account will be used to process the data instead of the Fivetran-managed service account.", - }, - "private_key": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - Description: "Private access key. The field should be specified if authentication type is `KEY_PAIR`.", - }, - "public_key": { - Type: schema.TypeString, - Computed: true, - Description: "Public key to grant Fivetran SSH access to git repository.", - }, - "cluster_id": { - Type: schema.TypeString, - Optional: true, - Description: "Cluster ID. Must be populated if `connection_type` is set to `SshTunnel` and `auth_type` is set to `IAM`.", - }, - "cluster_region": { - Type: schema.TypeString, - Optional: true, - Description: "Cluster region. Must be populated if `connection_type` is set to `SshTunnel` and `auth_type` is set to `IAM`.", - }, - "role": { - Type: schema.TypeString, - Optional: true, - Description: "The group role that you would like to assign this new user to. Supported group roles: ‘Destination Administrator‘, ‘Destination Reviewer‘, ‘Destination Analyst‘, ‘Connector Creator‘, or a custom destination role", - }, - "is_private_key_encrypted": { - Type: schema.TypeString, - Optional: true, - Computed: true, - Description: "Indicates that a private key is encrypted. The default value: `false`. The field can be specified if authentication type is `KEY_PAIR`.", - }, - "passphrase": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - Description: "In case private key is encrypted, you are required to enter passphrase that was used to encrypt the private key. The field can be specified if authentication type is `KEY_PAIR`.", - }, - "catalog": { - Type: schema.TypeString, - Optional: true, - Description: "Catalog name", - }, - "fivetran_role_arn": { - Type: schema.TypeString, - Optional: true, - Description: "ARN of the role which you created with different required policy mentioned in our setup guide", - }, - "prefix_path": { - Type: schema.TypeString, - Optional: true, - Description: "Prefix path of the bucket for which you have configured access policy. It is not required if access has been granted to entire Bucket in the access policy", - }, - "region": { - Type: schema.TypeString, - Optional: true, - Description: "Region of your AWS S3 bucket", - }, - }, - }, - } -} - -func dataSourceDestinationRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - var diags diag.Diagnostics - client := m.(*fivetran.Client) - svc := client.NewDestinationDetails() - - resp, err := svc.DestinationID(d.Get("id").(string)).Do(ctx) - if err != nil { - return newDiagAppend(diags, diag.Error, "service error", fmt.Sprintf("%v; code: %v; message: %v", err, resp.Code, resp.Message)) - } - - // msi stands for Map String Interface - msi := make(map[string]interface{}) - msi["id"] = resp.Data.ID - msi["group_id"] = resp.Data.GroupID - msi["service"] = resp.Data.Service - msi["region"] = resp.Data.Region - msi["time_zone_offset"] = resp.Data.TimeZoneOffset - config, err := dataSourceDestinationConfig(&resp) - if err != nil { - return newDiagAppend(diags, diag.Error, "set error", fmt.Sprint(err)) - } - msi["config"] = config - msi["setup_status"] = resp.Data.SetupStatus - for k, v := range msi { - if err := d.Set(k, v); err != nil { - return newDiagAppend(diags, diag.Error, "set error", fmt.Sprint(err)) - } - } - - d.SetId(resp.Data.ID) - - return diags -} - -// dataSourceDestinationConfig receives a *fivetran.DestinationDetailsResponse and returns a []interface{} -// containing the data type accepted by the "config" set. -func dataSourceDestinationConfig(resp *destinations.DestinationDetailsResponse) ([]interface{}, error) { - var config []interface{} - - c := make(map[string]interface{}) - c["host"] = resp.Data.Config.Host - if resp.Data.Config.Port != "" { - port, err := strconv.Atoi(resp.Data.Config.Port) - if err != nil { - return config, err - } - c["port"] = port - } - c["database"] = resp.Data.Config.Database - c["auth"] = resp.Data.Config.Auth - c["user"] = resp.Data.Config.User - c["password"] = resp.Data.Config.Password - c["connection_type"] = dataSourceDestinationConfigNormalizeConnectionType(resp.Data.Config.ConnectionType) - c["tunnel_host"] = resp.Data.Config.TunnelHost - c["tunnel_port"] = resp.Data.Config.TunnelPort - c["tunnel_user"] = resp.Data.Config.TunnelUser - c["project_id"] = resp.Data.Config.ProjectID - - // BQ returns its data_set_location as location in response - if resp.Data.Config.Location != "" && resourceDestinationIsBigQuery(resp.Data.Service) { - c["data_set_location"] = resp.Data.Config.Location - } else { - c["data_set_location"] = resp.Data.Config.DataSetLocation - } - - c["bucket"] = resp.Data.Config.Bucket - c["server_host_name"] = resp.Data.Config.ServerHostName - c["http_path"] = resp.Data.Config.HTTPPath - c["personal_access_token"] = resp.Data.Config.PersonalAccessToken - c["create_external_tables"] = resp.Data.Config.CreateExternalTables - c["external_location"] = resp.Data.Config.ExternalLocation - c["auth_type"] = resp.Data.Config.AuthType - c["role_arn"] = resp.Data.Config.RoleArn - c["secret_key"] = resp.Data.Config.SecretKey - c["private_key"] = resp.Data.Config.PrivateKey - c["public_key"] = resp.Data.Config.PublicKey - c["cluster_id"] = resp.Data.Config.ClusterId - c["cluster_region"] = resp.Data.Config.ClusterRegion - c["role"] = resp.Data.Config.Role - c["is_private_key_encrypted"] = resp.Data.Config.IsPrivateKeyEncrypted - c["passphrase"] = resp.Data.Config.Passphrase - c["catalog"] = resp.Data.Config.Catalog - c["fivetran_role_arn"] = resp.Data.Config.FivetranRoleArn - c["prefix_path"] = resp.Data.Config.PrefixPath - c["region"] = resp.Data.Config.Region - - config = append(config, c) - - return config, nil -} - -// dataSourceDestinationConfigNormalizeConnectionType normalizes *fivetran.DestinationDetailsResponse.Data.Config.ConnectionType. /T-111758. -func dataSourceDestinationConfigNormalizeConnectionType(connectionType string) string { - if connectionType == "SshTunnel" { - return "SSHTunnel" + ReadContext: resourceDestinationRead, + Schema: getDestinationSchema(true), } - return connectionType -} +} \ No newline at end of file diff --git a/fivetran/resource_destination.go b/fivetran/resource_destination.go index 31b26bfc..c3012ac2 100644 --- a/fivetran/resource_destination.go +++ b/fivetran/resource_destination.go @@ -19,62 +19,7 @@ func resourceDestination() *schema.Resource { UpdateWithoutTimeout: resourceDestinationUpdate, DeleteContext: resourceDestinationDelete, Importer: &schema.ResourceImporter{StateContext: schema.ImportStatePassthroughContext}, - Schema: map[string]*schema.Schema{ - "id": { - Type: schema.TypeString, - Computed: true, - Description: "The unique identifier for the destination within the Fivetran system.", - }, - "group_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "The unique identifier for the Group within the Fivetran system.", - }, - "service": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "The destination type name within the Fivetran system.", - }, - "region": { - Type: schema.TypeString, - Required: true, - Description: "Data processing location. This is where Fivetran will operate and run computation on data.", - }, - "time_zone_offset": { - Type: schema.TypeString, - Required: true, - Description: "Determines the time zone for the Fivetran sync schedule.", - }, - "config": resourceDestinationSchemaConfig(), - "trust_certificates": { - Type: schema.TypeBool, - Optional: true, - Description: "Specifies whether we should trust the certificate automatically. The default value is FALSE. If a certificate is not trusted automatically, it has to be approved with [Certificates Management API Approve a destination certificate](https://fivetran.com/docs/rest-api/certificates#approveadestinationcertificate).", - }, - "trust_fingerprints": { - Type: schema.TypeBool, - Optional: true, - Description: "Specifies whether we should trust the SSH fingerprint automatically. The default value is FALSE. If a fingerprint is not trusted automatically, it has to be approved with [Certificates Management API Approve a destination fingerprint](https://fivetran.com/docs/rest-api/certificates#approveadestinationfingerprint).", - }, - "run_setup_tests": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Specifies whether the setup tests should be run automatically. The default value is TRUE.", - }, - "setup_status": { - Type: schema.TypeString, - Computed: true, - Description: "Destination setup status", - }, - "last_updated": { - Type: schema.TypeString, - Computed: true, - Description: "", - }, // internal - }, + Schema: getDestinationSchema(false), Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(30 * time.Minute), Update: schema.DefaultTimeout(30 * time.Minute), @@ -82,8 +27,82 @@ func resourceDestination() *schema.Resource { } } -func resourceDestinationSchemaConfig() *schema.Schema { - return &schema.Schema{Type: schema.TypeList, Required: true, MaxItems: 1, +func getDestinationSchema(datasource bool) map[string]*schema.Schema { + return map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: !datasource, + Required: datasource, + Description: "The unique identifier for the destination within the Fivetran system.", + }, + "group_id": { + Type: schema.TypeString, + Required: !datasource, + ForceNew: !datasource, + Computed: datasource, + Description: "The unique identifier for the Group within the Fivetran system.", + }, + "service": { + Type: schema.TypeString, + Required: !datasource, + ForceNew: !datasource, + Computed: datasource, + Description: "The destination type name within the Fivetran system.", + }, + "region": { + Type: schema.TypeString, + Required: !datasource, + Computed: datasource, + Description: "Data processing location. This is where Fivetran will operate and run computation on data.", + }, + "time_zone_offset": { + Type: schema.TypeString, + Required: !datasource, + Computed: datasource, + Description: "Determines the time zone for the Fivetran sync schedule.", + }, + "config": getDestinationSchemaConfig(datasource), + "trust_certificates": { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies whether we should trust the certificate automatically. The default value is FALSE. If a certificate is not trusted automatically, it has to be approved with [Certificates Management API Approve a destination certificate](https://fivetran.com/docs/rest-api/certificates#approveadestinationcertificate).", + }, + "trust_fingerprints": { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies whether we should trust the SSH fingerprint automatically. The default value is FALSE. If a fingerprint is not trusted automatically, it has to be approved with [Certificates Management API Approve a destination fingerprint](https://fivetran.com/docs/rest-api/certificates#approveadestinationfingerprint).", + }, + "run_setup_tests": { + Type: schema.TypeBool, + Optional: true, + Default: datasource, + Description: "Specifies whether the setup tests should be run automatically. The default value is TRUE.", + }, + "setup_status": { + Type: schema.TypeString, + Computed: true, + Description: "Destination setup status", + }, + "last_updated": { + Type: schema.TypeString, + Computed: true, + Description: "", + }, + } +} + +func getDestinationSchemaConfig(datasource bool) *schema.Schema { + maxItems := 1 + if datasource { + maxItems = 0 + } + + return &schema.Schema{ + Type: schema.TypeList, + Required: !datasource, + Optional: datasource, + Computed: datasource, + MaxItems: maxItems, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "host": { @@ -317,6 +336,7 @@ func resourceDestinationRead(ctx context.Context, d *schema.ResourceData, m inte msi["service"] = resp.Data.Service msi["region"] = resp.Data.Region msi["time_zone_offset"] = resp.Data.TimeZoneOffset + config, err := resourceDestinationReadConfig(&resp, d.Get("config").([]interface{})) if err != nil { return newDiagAppend(diags, diag.Error, "set error", fmt.Sprint(err)) @@ -329,6 +349,8 @@ func resourceDestinationRead(ctx context.Context, d *schema.ResourceData, m inte } } + d.SetId(resp.Data.ID) + return diags } @@ -444,15 +466,22 @@ func resourceDestinationReadConfig(resp *destinations.DestinationDetailsResponse // if `is_private_key_encrypted` is configured locally we should read upstream value c["is_private_key_encrypted"] = resp.Data.Config.IsPrivateKeyEncrypted } + } else { + c["password"] = resp.Data.Config.Password + c["personal_access_token"] = resp.Data.Config.PersonalAccessToken + c["role_arn"] = resp.Data.Config.RoleArn + c["secret_key"] = resp.Data.Config.SecretKey + c["private_key"] = resp.Data.Config.PrivateKey + c["passphrase"] = resp.Data.Config.Passphrase + + if strToBool(resp.Data.Config.IsPrivateKeyEncrypted) { + // we should ignore default `false` value if not configured to prevent data drifts + // we read it only if `true` to prevent false data drifts + c["is_private_key_encrypted"] = resp.Data.Config.IsPrivateKeyEncrypted + } } - if strToBool(resp.Data.Config.IsPrivateKeyEncrypted) { - // we should ignore default `false` value if not configured to prevent data drifts - // we read it only if `true` to prevent false data drifts - c["is_private_key_encrypted"] = resp.Data.Config.IsPrivateKeyEncrypted - } - - c["connection_type"] = dataSourceDestinationConfigNormalizeConnectionType(resp.Data.Config.ConnectionType) + c["connection_type"] = resourceDestinationConfigNormalizeConnectionType(resp.Data.Config.ConnectionType) c["tunnel_host"] = resp.Data.Config.TunnelHost c["tunnel_port"] = resp.Data.Config.TunnelPort c["tunnel_user"] = resp.Data.Config.TunnelUser @@ -625,3 +654,10 @@ func resourceDestinationCreateConfig(config []interface{}) (*destinations.Destin return fivetranConfig, hasConfig } + +func resourceDestinationConfigNormalizeConnectionType(connectionType string) string { + if connectionType == "SshTunnel" { + return "SSHTunnel" + } + return connectionType +} diff --git a/fivetran/resource_user_test.go b/fivetran/resource_user_test.go index e51f6992..ed388deb 100644 --- a/fivetran/resource_user_test.go +++ b/fivetran/resource_user_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" @@ -97,10 +98,10 @@ func testFivetranUserResourceDestroy(s *terraform.State) error { } response, err := client.NewUserDetails().UserID(rs.Primary.ID).Do(context.Background()) - if err.Error() != "status code: 404; expected: 200" { + if !strings.HasPrefix(err.Error(), "status code: 404; expected: 200") { return err } - if response.Code != "NotFound" { + if !strings.HasPrefix(response.Code, "NotFound") { return errors.New("User " + rs.Primary.ID + " still exists.") } diff --git a/fivetran/resource_webhook_test.go b/fivetran/resource_webhook_test.go index d74cdf78..efc9c621 100644 --- a/fivetran/resource_webhook_test.go +++ b/fivetran/resource_webhook_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" @@ -99,10 +100,10 @@ func testFivetranWebhookResourceDestroy(s *terraform.State) error { } response, err := client.NewWebhookDetails().WebhookId(rs.Primary.ID).Do(context.Background()) - if err.Error() != "status code: 404; expected: 200" { + if !strings.HasPrefix(err.Error(), "status code: 404; expected: 200") { return err } - if response.Code != "NotFound" { + if !strings.HasPrefix(response.Code, "NotFound") { return errors.New("Webhook " + rs.Primary.ID + " still exists.") } diff --git a/fivetran/tests/mock/datasource_destination_test.go b/fivetran/tests/mock/datasource_destination_test.go index b88afef7..197c4b93 100644 --- a/fivetran/tests/mock/datasource_destination_test.go +++ b/fivetran/tests/mock/datasource_destination_test.go @@ -72,7 +72,6 @@ func TestDataSourceDestinationConfigMappingMock(t *testing.T) { resource.TestCheckResourceAttr("data.fivetran_destination.test_destintion", "config.0.cluster_id", "cluster_id"), resource.TestCheckResourceAttr("data.fivetran_destination.test_destintion", "config.0.cluster_region", "cluster_region"), resource.TestCheckResourceAttr("data.fivetran_destination.test_destintion", "config.0.role", "role"), - resource.TestCheckResourceAttr("data.fivetran_destination.test_destintion", "config.0.is_private_key_encrypted", "false"), resource.TestCheckResourceAttr("data.fivetran_destination.test_destintion", "config.0.passphrase", "******"), resource.TestCheckResourceAttr("data.fivetran_destination.test_destintion", "config.0.catalog", "catalog"), resource.TestCheckResourceAttr("data.fivetran_destination.test_destintion", "config.0.fivetran_role_arn", "fivetran_role_arn"), diff --git a/go.mod b/go.mod index 162f3b4b..868fd65d 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/fivetran/terraform-provider-fivetran require ( - github.com/fivetran/go-fivetran v0.7.12 + github.com/fivetran/go-fivetran v0.7.13 github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.0 ) diff --git a/go.sum b/go.sum index 13b01349..0a9ba428 100644 --- a/go.sum +++ b/go.sum @@ -91,6 +91,8 @@ github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fivetran/go-fivetran v0.7.12 h1:XX4ZqQUgiWCZQeFWxUYCmgY2MowPPVHKhf1CMw2+bvI= github.com/fivetran/go-fivetran v0.7.12/go.mod h1:EIy5Uwn1zylQCr/7O+8rrwvmjvhW3PPpzHkQj26ON7Y= +github.com/fivetran/go-fivetran v0.7.13 h1:FYb4+5OzAFGnUnrTLDElyA8c0ZmI3WGYv+KuKWsaVPw= +github.com/fivetran/go-fivetran v0.7.13/go.mod h1:EIy5Uwn1zylQCr/7O+8rrwvmjvhW3PPpzHkQj26ON7Y= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=