diff --git a/internal/services/orbital/contact_resource.go b/internal/services/orbital/contact_resource.go new file mode 100644 index 000000000000..86170c29299d --- /dev/null +++ b/internal/services/orbital/contact_resource.go @@ -0,0 +1,207 @@ +package orbital + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/resource-manager/orbital/2022-03-01/contact" + "github.com/hashicorp/go-azure-sdk/resource-manager/orbital/2022-03-01/contactprofile" + "github.com/hashicorp/go-azure-sdk/resource-manager/orbital/2022-03-01/spacecraft" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type ContactResource struct{} + +type ContactResourceModel struct { + Name string `tfschema:"name"` + Spacecraft string `tfschema:"spacecraft_id"` + ReservationStartTime string `tfschema:"reservation_start_time"` + ReservationEndTime string `tfschema:"reservation_end_time"` + GroundStationName string `tfschema:"ground_station_name"` + ContactProfileId string `tfschema:"contact_profile_id"` +} + +func (r ContactResource) Arguments() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "spacecraft_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: spacecraft.ValidateSpacecraftID, + }, + + "reservation_start_time": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "reservation_end_time": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "ground_station_name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "contact_profile_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: contactprofile.ValidateContactProfileID, + }, + } +} + +func (r ContactResource) Attributes() map[string]*schema.Schema { + return map[string]*schema.Schema{} +} + +func (r ContactResource) ModelObject() interface{} { + return &ContactResourceModel{} +} + +func (r ContactResource) ResourceType() string { + return "azurerm_orbital_contact" +} + +func (r ContactResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var model ContactResourceModel + if err := metadata.Decode(&model); err != nil { + return err + } + + client := metadata.Client.Orbital.ContactClient + subscriptionId := metadata.Client.Account.SubscriptionId + + spacecraftId, err := contact.ParseSpacecraftID(model.Spacecraft) + if err != nil { + return err + } + id := contact.NewContactID(subscriptionId, spacecraftId.ResourceGroupName, spacecraftId.SpacecraftName, model.Name) + existing, err := client.Get(ctx, id) + if err != nil && !response.WasNotFound(existing.HttpResponse) { + return fmt.Errorf("checking for presence of existing %s: %+v", id, err) + } + + if !response.WasNotFound(existing.HttpResponse) { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + + contactProfile := contact.ResourceReference{ + Id: &model.ContactProfileId, + } + + contactProperties := contact.ContactsProperties{ + ContactProfile: contactProfile, + GroundStationName: model.GroundStationName, + ReservationEndTime: model.ReservationEndTime, + ReservationStartTime: model.ReservationStartTime, + } + + contact := contact.Contact{ + Id: utils.String(id.ID()), + Name: utils.String(model.Name), + Properties: &contactProperties, + } + if _, err = client.Create(ctx, id, contact); err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + metadata.SetID(id) + return nil + }, + } +} + +func (r ContactResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Orbital.ContactClient + id, err := contact.ParseContactID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + resp, err := client.Get(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("reading %s: %+v", *id, err) + } + + spacecraftId := contact.NewSpacecraftID(id.SubscriptionId, id.ResourceGroupName, id.SpacecraftName) + if model := resp.Model; model != nil { + props := model.Properties + state := ContactResourceModel{ + Name: id.ContactName, + Spacecraft: spacecraftId.ID(), + } + + if props != nil { + state.ReservationStartTime = props.ReservationStartTime + state.ReservationEndTime = props.ReservationEndTime + state.GroundStationName = props.GroundStationName + if props.ContactProfile.Id != nil { + state.ContactProfileId = *props.ContactProfile.Id + } else { + return fmt.Errorf("contact profile id is missing %s", *id) + } + } else { + return fmt.Errorf("required properties are missing %s", *id) + } + + return metadata.Encode(&state) + } + return nil + }, + } +} + +func (r ContactResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Orbital.ContactClient + id, err := contact.ParseContactID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + metadata.Logger.Infof("deleting %s", *id) + + if _, err := client.Delete(ctx, *id); err != nil { + return fmt.Errorf("deleting %s: %+v", *id, err) + } + return nil + }, + } +} + +func (r ContactResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return contact.ValidateContactID +} diff --git a/internal/services/orbital/contact_resource_test.go b/internal/services/orbital/contact_resource_test.go new file mode 100644 index 000000000000..366939cf7f46 --- /dev/null +++ b/internal/services/orbital/contact_resource_test.go @@ -0,0 +1,139 @@ +package orbital_test + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/resource-manager/orbital/2022-03-01/contact" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type ContactResource struct{} + +func TestAccContact_basic(t *testing.T) { + skipContact(t) + + data := acceptance.BuildTestData(t, "azurerm_orbital_contact", "test") + r := ContactResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (r ContactResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := contact.ParseContactID(state.ID) + if err != nil { + return nil, err + } + + resp, err := client.Orbital.ContactClient.Get(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return utils.Bool(false), nil + } + return nil, fmt.Errorf("retrieving %s: %+v", *id, err) + } + return utils.Bool(true), nil +} + +func (r ContactResource) basic(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%[1]s + +resource "azurerm_orbital_contact" "test" { + name = "testcontact-%[2]d" + spacecraft_id = "%[3]s" + reservation_start_time = "2025-07-16T20:35:00Z" + reservation_end_time = "2025-07-16T20:55:00Z" + ground_station_name = "Microsoft_Quincy" + contact_profile_id = azurerm_orbital_contact_profile.test.id +} +`, template, data.RandomInteger, os.Getenv("ARM_TEST_SPACECRAFT_ID")) +} + +func (r ContactResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} + +resource "azurerm_virtual_network" "test" { + name = "testvnet" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test" { + name = "testsubnet" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.1.0/24"] + + delegation { + name = "orbitalgateway" + + service_delegation { + name = "Microsoft.Orbital/orbitalGateways" + actions = [ + "Microsoft.Network/publicIPAddresses/join/action", + "Microsoft.Network/virtualNetworks/subnets/join/action", + "Microsoft.Network/virtualNetworks/read", + "Microsoft.Network/publicIPAddresses/read", + ] + } + } +} + +resource "azurerm_orbital_contact_profile" "test" { + name = "testcontactprofile-%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + minimum_variable_contact_duration = "PT1M" + auto_tracking = "disabled" + links { + channels { + name = "channelname" + bandwidth_mhz = 100 + center_frequency_mhz = 101 + end_point { + end_point_name = "AQUA_command" + ip_address = "10.0.1.0" + port = "49153" + protocol = "TCP" + } + } + direction = "Uplink" + name = "RHCP_UL" + polarization = "RHCP" + } + network_configuration_subnet_id = azurerm_subnet.test.id +} +`, data.RandomInteger, data.Locations.Primary) +} + +func skipContact(t *testing.T) { + if os.Getenv("ARM_TEST_SPACECRAFT_ID") == "" { + t.Skip("Skipping as `ARM_TEST_SPACECRAFT_ID` was not specified") + } +} diff --git a/internal/services/orbital/registration.go b/internal/services/orbital/registration.go index 6e8869751b35..f43c3669710b 100644 --- a/internal/services/orbital/registration.go +++ b/internal/services/orbital/registration.go @@ -22,6 +22,7 @@ func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ SpacecraftResource{}, ContactProfileResource{}, + ContactResource{}, } } diff --git a/website/docs/r/orbital_contact.html.markdown b/website/docs/r/orbital_contact.html.markdown new file mode 100644 index 000000000000..22a001a8eed8 --- /dev/null +++ b/website/docs/r/orbital_contact.html.markdown @@ -0,0 +1,143 @@ +--- +subcategory: "Orbital" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_orbital_contact_profile" +description: |- + Manages an orbital contact resource. +--- + +# azurerm_orbital_contact + +Manages an orbital contact. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "rg-example" + location = "West Europe" +} + +resource "azurerm_orbital_spacecraft" "example" { + name = "example-spacecraft" + resource_group_name = azurerm_resource_group.test.name + location = "westeurope" + norad_id = "12345" + + links { + bandwidth_mhz = 100 + center_frequency_mhz = 101 + direction = "Uplink" + polarization = "LHCP" + name = "examplename" + } + + two_line_elements = ["1 23455U 94089A 97320.90946019 .00000140 00000-0 10191-3 0 2621", "2 23455 99.0090 272.6745 0008546 223.1686 136.8816 14.11711747148495"] + title_line = "AQUA" + + tags = { + aks-managed-cluster-name = "9a57225d-a405-4d40-aa46-f13d2342abef" + } +} + +resource "azurerm_virtual_network" "example" { + name = "example-vnet" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name +} + +resource "azurerm_subnet" "example" { + name = "example-subnet" + resource_group_name = azurerm_resource_group.example.name + virtual_network_name = azurerm_virtual_network.example.name + address_prefixes = ["10.0.1.0/24"] + + delegation { + name = "orbitalgateway" + + service_delegation { + name = "Microsoft.Orbital/orbitalGateways" + actions = [ + "Microsoft.Network/publicIPAddresses/join/action", + "Microsoft.Network/virtualNetworks/subnets/join/action", + "Microsoft.Network/virtualNetworks/read", + "Microsoft.Network/publicIPAddresses/read", + ] + } + } +} + +resource "azurerm_orbital_contact_profile" "example" { + name = "example-contactprofile" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + minimum_variable_contact_duration = "PT1M" + auto_tracking = "disabled" + links { + channels { + name = "channelname" + bandwidth_mhz = 100 + center_frequency_mhz = 101 + end_point { + end_point_name = "AQUA_command" + ip_address = "10.0.1.0" + port = "49153" + protocol = "TCP" + } + } + direction = "Uplink" + name = "RHCP_UL" + polarization = "RHCP" + } + network_configuration_subnet_id = azurerm_subnet.example.id +} + +resource "azurerm_orbital_contact" "example" { + name = "example-contact" + spacecraft_id = azurerm_orbital_spacecraft.example.id + reservation_start_time = "2020-07-16T20:35:00.00Z" + reservation_end_time = "2020-07-16T20:55:00.00Z" + ground_station_name = "WESTUS2_0" + contact_profile_id = azurerm_orbital_contact_profile.example.id +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `name` - (Required) The name of the Contact. Changing this forces a new resource to be created. Changing this forces a new resource to be created. + +* `spacecraft_id` - (Required) The ID of the spacecraft which the contact will be made to. Changing this forces a new resource to be created. + +* `reservation_start_time` - (Required) Reservation start time of the Contact. Changing this forces a new resource to be created. + +* `reservation_end_time` - (Required) Reservation end time of the Contact. Changing this forces a new resource to be created. + +* `ground_station_name` - (Required) Name of the Azure ground station. Changing this forces a new resource to be created. + +* `contact_profile_id` - (Required) ID of the orbital contact profile. Changing this forces a new resource to be created. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Contact. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Contact. +* `read` - (Defaults to 5 minutes) Used when retrieving the Contact. +* `update` - (Defaults to 30 minutes) Used when updating the Contact. +* `delete` - (Defaults to 30 minutes) Used when deleting the Contact. + +## Import + +Spacecraft can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_orbital_contact.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Orbital/spacecrafts/spacecraft1/contacts/contact1 +```