diff --git a/cloudconnexa/data_source_application.go b/cloudconnexa/data_source_application.go new file mode 100644 index 0000000..3614589 --- /dev/null +++ b/cloudconnexa/data_source_application.go @@ -0,0 +1,58 @@ +package cloudconnexa + +import ( + "context" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" +) + +func dataSourceApplication() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceApplicationRead, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "routes": { + Type: schema.TypeList, + Computed: true, + Elem: resourceApplicationRoute(), + }, + "config": { + Type: schema.TypeList, + Computed: true, + Elem: resourceApplicationConfig(), + }, + "network_item_type": { + Type: schema.TypeString, + Computed: true, + }, + "network_item_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceApplicationRead(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { + c := i.(*cloudconnexa.Client) + var diags diag.Diagnostics + var name = data.Get("name").(string) + application, err := c.Applications.GetByName(name) + + if err != nil { + return diag.FromErr(err) + } + if application == nil { + return append(diags, diag.Errorf("Application with name %s was not found", name)...) + } + setApplicationData(data, application) + return nil +} diff --git a/cloudconnexa/data_source_host.go b/cloudconnexa/data_source_host.go index 54d2d65..f095c89 100644 --- a/cloudconnexa/data_source_host.go +++ b/cloudconnexa/data_source_host.go @@ -15,6 +15,11 @@ func dataSourceHost() *schema.Resource { Description: "Use an `cloudconnexa_host` data source to read an existing CloudConnexa connector.", ReadContext: dataSourceHostRead, Schema: map[string]*schema.Schema{ + "host_id": { + Type: schema.TypeString, + Computed: true, + Description: "The host ID.", + }, "name": { Type: schema.TypeString, Required: true, @@ -88,6 +93,7 @@ func dataSourceHostRead(ctx context.Context, d *schema.ResourceData, m interface if err != nil { return append(diags, diag.FromErr(err)...) } + d.Set("host_id", host.Id) d.Set("name", host.Name) d.Set("internet_access", host.InternetAccess) d.Set("system_subnets", host.SystemSubnets) diff --git a/cloudconnexa/provider.go b/cloudconnexa/provider.go index 4f9237b..2f80469 100644 --- a/cloudconnexa/provider.go +++ b/cloudconnexa/provider.go @@ -45,14 +45,15 @@ func Provider() *schema.Provider { }, }, ResourcesMap: map[string]*schema.Resource{ - "cloudconnexa_network": resourceNetwork(), - "cloudconnexa_connector": resourceConnector(), - "cloudconnexa_route": resourceRoute(), - "cloudconnexa_dns_record": resourceDnsRecord(), - "cloudconnexa_user": resourceUser(), - "cloudconnexa_host": resourceHost(), - "cloudconnexa_user_group": resourceUserGroup(), - "cloudconnexa_ip_service": resourceIPService(), + "cloudconnexa_network": resourceNetwork(), + "cloudconnexa_connector": resourceConnector(), + "cloudconnexa_route": resourceRoute(), + "cloudconnexa_dns_record": resourceDnsRecord(), + "cloudconnexa_user": resourceUser(), + "cloudconnexa_host": resourceHost(), + "cloudconnexa_user_group": resourceUserGroup(), + "cloudconnexa_ip_service": resourceIPService(), + "cloudconnexa_application": resourceApplication(), }, DataSourcesMap: map[string]*schema.Resource{ @@ -64,6 +65,7 @@ func Provider() *schema.Provider { "cloudconnexa_network_routes": dataSourceNetworkRoutes(), "cloudconnexa_host": dataSourceHost(), "cloudconnexa_ip_service": dataSourceIPService(), + "cloudconnexa_application": dataSourceApplication(), }, ConfigureContextFunc: providerConfigure, } diff --git a/cloudconnexa/resource_application.go b/cloudconnexa/resource_application.go new file mode 100644 index 0000000..4e36d05 --- /dev/null +++ b/cloudconnexa/resource_application.go @@ -0,0 +1,299 @@ +package cloudconnexa + +import ( + "context" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" +) + +func resourceApplication() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceApplicationCreate, + ReadContext: resourceApplicationRead, + DeleteContext: resourceApplicationDelete, + UpdateContext: resourceApplicationUpdate, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 40), + }, + "description": { + Type: schema.TypeString, + Default: "Managed by Terraform", + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 120), + }, + "routes": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + Elem: resourceApplicationRoute(), + }, + "config": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: resourceApplicationConfig(), + }, + "network_item_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"NETWORK", "HOST"}, false), + }, + "network_item_id": { + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func resourceApplicationUpdate(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { + c := i.(*cloudconnexa.Client) + + s, err := c.Applications.Update(data.Id(), resourceDataToApplication(data)) + if err != nil { + return diag.FromErr(err) + } + setApplicationData(data, s) + return nil +} + +func resourceApplicationRoute() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "domain": { + Type: schema.TypeString, + Required: true, + }, + "allow_embedded_ip": { + Type: schema.TypeBool, + Optional: true, + }, + }, + } +} + +func resourceApplicationConfig() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "custom_service_types": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: customServiceTypesConfig(), + }, + }, + "service_types": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + + val := i.(string) + for _, validValue := range validValues { + if val == validValue { + return nil + } + } + return diag.Errorf("service type must be one of %s", validValues) + }, + }, + }, + }, + } +} + +func customServiceTypesConfig() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "protocol": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"TCP", "UDP", "ICMP"}, false), + }, + "from_port": { + Type: schema.TypeInt, + Optional: true, + }, + "to_port": { + Type: schema.TypeInt, + Optional: true, + }, + } +} + +func resourceApplicationRead(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { + c := i.(*cloudconnexa.Client) + var diags diag.Diagnostics + application, err := c.Applications.Get(data.Id()) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + if application == nil { + data.SetId("") + return diags + } + setApplicationData(data, application) + return diags +} + +func setApplicationData(data *schema.ResourceData, application *cloudconnexa.ApplicationResponse) { + data.SetId(application.Id) + _ = data.Set("name", application.Name) + _ = data.Set("description", application.Description) + _ = data.Set("routes", flattenApplicationRoutes(application.Routes)) + _ = data.Set("config", flattenApplicationConfig(application.Config)) + _ = data.Set("network_item_type", application.NetworkItemType) + _ = data.Set("network_item_id", application.NetworkItemId) +} + +func resourceApplicationDelete(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { + c := i.(*cloudconnexa.Client) + var diags diag.Diagnostics + err := c.Applications.Delete(data.Id()) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + return diags +} + +func flattenApplicationConfig(config *cloudconnexa.ApplicationConfig) interface{} { + var data = map[string]interface{}{ + "custom_service_types": flattenApplicationCustomTypes(config.CustomServiceTypes), + "service_types": config.ServiceTypes, + } + return []interface{}{data} +} + +func flattenApplicationCustomTypes(types []*cloudconnexa.CustomApplicationType) interface{} { + var cst []interface{} + for _, t := range types { + var ports = append(t.Port, t.IcmpType...) + if len(ports) > 0 { + for _, port := range ports { + cst = append(cst, map[string]interface{}{ + "protocol": t.Protocol, + "from_port": port.LowerValue, + "to_port": port.UpperValue, + }) + } + } else { + cst = append(cst, map[string]interface{}{ + "protocol": t.Protocol, + }) + } + } + return cst +} + +func flattenApplicationRoutes(routes []*cloudconnexa.Route) []map[string]interface{} { + var data []map[string]interface{} + for _, route := range routes { + data = append(data, map[string]interface{}{ + "domain": route.Domain, + "allow_embedded_ip": route.AllowEmbeddedIp, + }) + } + return data +} + +func resourceApplicationCreate(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(*cloudconnexa.Client) + + application := resourceDataToApplication(data) + createdApplication, err := client.Applications.Create(application) + if err != nil { + return diag.FromErr(err) + } + setApplicationData(data, createdApplication) + return nil +} + +func resourceDataToApplication(data *schema.ResourceData) *cloudconnexa.Application { + routes := data.Get("routes").([]interface{}) + var configRoutes []*cloudconnexa.ApplicationRoute + for _, r := range routes { + var route = r.(map[string]interface{}) + configRoutes = append( + configRoutes, + &cloudconnexa.ApplicationRoute{ + Value: route["domain"].(string), + AllowEmbeddedIp: route["allow_embedded_ip"].(bool), + }, + ) + } + + config := cloudconnexa.ApplicationConfig{} + configList := data.Get("config").([]interface{}) + if len(configList) > 0 && configList[0] != nil { + + config.CustomServiceTypes = []*cloudconnexa.CustomApplicationType{} + config.ServiceTypes = []string{} + + mainConfig := configList[0].(map[string]interface{}) + var cst = mainConfig["custom_service_types"].(*schema.Set) + var groupedCst = make(map[string][]cloudconnexa.Range) + for _, item := range cst.List() { + var cstItem = item.(map[string]interface{}) + var protocol = cstItem["protocol"].(string) + var fromPort = cstItem["from_port"].(int) + var toPort = cstItem["to_port"].(int) + + if groupedCst[protocol] == nil { + groupedCst[protocol] = make([]cloudconnexa.Range, 0) + } + if fromPort > 0 || toPort > 0 { + groupedCst[protocol] = append(groupedCst[protocol], cloudconnexa.Range{ + LowerValue: fromPort, + UpperValue: toPort, + }) + } + } + + for protocol, ports := range groupedCst { + if protocol == "ICMP" { + config.CustomServiceTypes = append( + config.CustomServiceTypes, + &cloudconnexa.CustomApplicationType{ + Protocol: protocol, + IcmpType: ports, + }, + ) + } else { + config.CustomServiceTypes = append( + config.CustomServiceTypes, + &cloudconnexa.CustomApplicationType{ + Protocol: protocol, + Port: ports, + }, + ) + } + } + + for _, r := range mainConfig["service_types"].([]interface{}) { + config.ServiceTypes = append(config.ServiceTypes, r.(string)) + } + } + + s := &cloudconnexa.Application{ + Name: data.Get("name").(string), + Description: data.Get("description").(string), + NetworkItemId: data.Get("network_item_id").(string), + NetworkItemType: data.Get("network_item_type").(string), + Routes: configRoutes, + Config: &config, + } + return s +} diff --git a/docs/data-sources/application.md b/docs/data-sources/application.md new file mode 100644 index 0000000..d9094d1 --- /dev/null +++ b/docs/data-sources/application.md @@ -0,0 +1,46 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cloudconnexa_application Data Source - terraform-provider-cloudconnexa" +subcategory: "" +description: |- + +--- + +# cloudconnexa_application (Data Source) + + + + + + +## Schema + +### Required + +- `name` (String) + +### Read-Only + +- `config` (List of Object) (see [below for nested schema](#nestedatt--config)) +- `description` (String) +- `id` (String) The ID of this resource. +- `network_item_id` (String) +- `network_item_type` (String) +- `routes` (List of String) + + +### Nested Schema for `config` + +Read-Only: + +- `custom_service_types` (Set of Object) (see [below for nested schema](#nestedobjatt--config--custom_service_types)) +- `service_types` (List of String) + + +### Nested Schema for `config.custom_service_types` + +Read-Only: + +- `from_port` (Number) +- `protocol` (String) +- `to_port` (Number) diff --git a/docs/resources/application.md b/docs/resources/application.md new file mode 100644 index 0000000..2696dda --- /dev/null +++ b/docs/resources/application.md @@ -0,0 +1,64 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cloudconnexa_application Resource - terraform-provider-cloudconnexa" +subcategory: "" +description: |- + +--- + +# cloudconnexa_application (Resource) + + + + + + +## Schema + +### Required + +- `name` (String) +- `network_item_id` (String) +- `network_item_type` (String) +- `routes` (Block List, Min: 1) (see [below for nested schema](#nestedblock--routes)) + +### Optional + +- `config` (Block List, Max: 1) (see [below for nested schema](#nestedblock--config)) +- `description` (String) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `routes` + +Required: + +- `domain` (String) + +Optional: + +- `allow_embedded_ip` (Boolean) + + + +### Nested Schema for `config` + +Optional: + +- `custom_service_types` (Block Set) (see [below for nested schema](#nestedblock--config--custom_service_types)) +- `service_types` (List of String) + + +### Nested Schema for `config.custom_service_types` + +Required: + +- `protocol` (String) + +Optional: + +- `from_port` (Number) +- `to_port` (Number) diff --git a/example/applications.tf b/example/applications.tf new file mode 100644 index 0000000..ea4bf03 --- /dev/null +++ b/example/applications.tf @@ -0,0 +1,88 @@ +data "cloudconnexa_network" "test-net" { + name = "test-net" +} + +resource "cloudconnexa_application" "application_full_access" { + name = "example-application-1" + network_item_type = "NETWORK" + network_item_id = data.cloudconnexa_network.test-net.network_id + routes { + domain = "example-application-1.com" + allow_embedded_ip = false + } + + config { + service_types = ["ANY"] + } +} + +resource "cloudconnexa_application" "application_custom_access" { + name = "example-application-2" + network_item_type = "NETWORK" + network_item_id = data.cloudconnexa_network.test-net.network_id + + routes { + domain = "example-application-2.com" + allow_embedded_ip = false + } + + config { + service_types = ["HTTP", "HTTPS", "CUSTOM"] + custom_service_types { + protocol = "TCP" //all tcp ports + } + custom_service_types { + protocol = "UDP" + from_port = 1194 + to_port = 1194 + } + custom_service_types { + protocol = "UDP" + from_port = 5000 + to_port = 5010 + } + custom_service_types { + protocol = "ICMP" + from_port = 8 + to_port = 8 + } + custom_service_types { + protocol = "ICMP" + from_port = 20 + to_port = 22 + } + } +} + +locals { + created_by = "managed by terraform" +} + +variable "application_custom_access_advanced" { + description = "xxx" + type = any + default = { + "example-application-3" = { route = [{ domain = "example-application-3.com", allow_embedded_ip = true }, { domain = "example-application-33.com", allow_embedded_ip = false }] } + "example-application-4" = { route = [{ domain = "example-application-4.com", allow_embedded_ip = false }] } + } +} + +resource "cloudconnexa_application" "application_custom_access_advanced" { + for_each = var.application_custom_access_advanced + name = each.key + description = try(each.value.description, local.created_by) + network_item_type = "NETWORK" + network_item_id = data.cloudconnexa_network.test-net.network_id + config { + service_types = ["ANY"] + } + + dynamic "routes" { + for_each = each.value.route + + content { + domain = routes.value.domain + allow_embedded_ip = routes.value.allow_embedded_ip + } + } +} diff --git a/go.mod b/go.mod index 44cb4b0..3b33588 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/gruntwork-io/terratest v0.46.1 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 - github.com/openvpn/cloudconnexa-go-client/v2 v2.0.4 + github.com/openvpn/cloudconnexa-go-client/v2 v2.0.8 github.com/stretchr/testify v1.9.0 ) diff --git a/go.sum b/go.sum index 0dba60f..546fc20 100644 --- a/go.sum +++ b/go.sum @@ -485,8 +485,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/openvpn/cloudconnexa-go-client/v2 v2.0.4 h1:0Z8eTHPDYUFH3kCA1DTemiUnlPK9FhEuJhZB3bG1usE= -github.com/openvpn/cloudconnexa-go-client/v2 v2.0.4/go.mod h1:udq5IDkgXvMO6mQUEFsLHzEyGGAduhO0jJvlb9f4JkE= +github.com/openvpn/cloudconnexa-go-client/v2 v2.0.8 h1:67NXu2WqNnE05fhrq1HXbQKFMidhnd7ts6SFeuZSkLo= +github.com/openvpn/cloudconnexa-go-client/v2 v2.0.8/go.mod h1:udq5IDkgXvMO6mQUEFsLHzEyGGAduhO0jJvlb9f4JkE= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=