Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for azure resources integration TF resource #395

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/unreleased/Feature-20240708-095316.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Feature
body: Add support for configuring the OpsLevel Azure Resources Integration
time: 2024-07-08T09:53:16.629637-04:00
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import opslevel_integration_azure_resources.example Z2lkOi8vb3BzbGV2ZWwvU2VydmljZS82MDI0
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "opslevel_integration_azure_resources" "dev" {
name = "Azure Integration"
client_id = "XXX_CLIENT_ID_XXX"
client_secret = "XXX_CLIENT_SECRET_XXX"
tenant_id = "XXX_TENANT_ID_XXX"
subscription_id = "XXX_SUBSCRIPTION_ID_XXX"
}
1 change: 1 addition & 0 deletions opslevel/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ func (p *OpslevelProvider) Resources(context.Context) []func() resource.Resource
NewFilterResource,
NewInfrastructureResource,
NewIntegrationAwsResource,
NewIntegrationAzureResourcesResource,
NewPropertyAssignmentResource,
NewPropertyDefinitionResource,
NewRepositoryResource,
Expand Down
224 changes: 224 additions & 0 deletions opslevel/resource_opslevel_integration_azure_resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package opslevel

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/opslevel/opslevel-go/v2024"
)

var _ resource.ResourceWithConfigure = &IntegrationAzureResourcesResource{}

var _ resource.ResourceWithImportState = &IntegrationAzureResourcesResource{}

func NewIntegrationAzureResourcesResource() resource.Resource {
taimoorgit marked this conversation as resolved.
Show resolved Hide resolved
return &IntegrationAzureResourcesResource{}
}

type IntegrationAzureResourcesResource struct {
CommonResourceClient
}

type IntegrationAzureResourcesResourceModel struct {
Aliases types.List `tfsdk:"aliases"`
ClientId types.String `tfsdk:"client_id"`
ClientSecret types.String `tfsdk:"client_secret"`
Id types.String `tfsdk:"id"`
LastSyncedAt types.String `tfsdk:"last_synced_at"`
Name types.String `tfsdk:"name"`
SubscriptionId types.String `tfsdk:"subscription_id"`
TenantId types.String `tfsdk:"tenant_id"`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we also added ownershipTagKeys and tagsOverrideOwnership to the mutation and integration type

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call out. This has been added in the upstream opslevel-go PR and I have not yet updated here

}

func NewIntegrationAzureResourcesResourceModel(ctx context.Context, azureResourcesIntegration opslevel.Integration, givenModel IntegrationAzureResourcesResourceModel) (IntegrationAzureResourcesResourceModel, diag.Diagnostics) {
resourceModel := IntegrationAzureResourcesResourceModel{
ClientId: givenModel.ClientId,
ClientSecret: givenModel.ClientSecret,
Id: ComputedStringValue(string(azureResourcesIntegration.Id)),
Name: RequiredStringValue(azureResourcesIntegration.Name),
SubscriptionId: RequiredStringValue(azureResourcesIntegration.SubscriptionId),
TenantId: RequiredStringValue(azureResourcesIntegration.TenantId),
}

if azureResourcesIntegration.LastSyncedAt == nil {
resourceModel.LastSyncedAt = types.StringNull()
} else {
resourceModel.LastSyncedAt = types.StringValue(azureResourcesIntegration.LastSyncedAt.String())
}
var diags diag.Diagnostics
resourceModel.Aliases, diags = types.ListValueFrom(ctx, types.StringType, azureResourcesIntegration.Aliases)

return resourceModel, diags
}

func (r *IntegrationAzureResourcesResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_integration_azure_resources"
taimoorgit marked this conversation as resolved.
Show resolved Hide resolved
}

func (r *IntegrationAzureResourcesResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Azure Resources Integration resource",
taimoorgit marked this conversation as resolved.
Show resolved Hide resolved

Attributes: map[string]schema.Attribute{
"client_id": schema.StringAttribute{
Description: "The client id OpsLevel uses to access the Azure account.",
Required: true,
},
"client_secret": schema.StringAttribute{
Description: "The client secret OpsLevel uses to access the Azure account.",
Required: true,
Sensitive: true,
},
"tenant_id": schema.StringAttribute{
Description: "The tenant OpsLevel uses to access the Azure account.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"subscription_id": schema.StringAttribute{
Description: "The subscription OpsLevel uses to access the Azure account.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"last_synced_at": schema.StringAttribute{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't a field on the AWS integration - are we sure it'll be in the public API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rocktavious this will be in the public schema and this is for the Azure Resources integration only.

Description: "The time the Integration last imported data from Azure.",
Computed: true,
},
"aliases": schema.ListAttribute{
ElementType: types.StringType,
Description: "All of the aliases attached to the resource.",
Computed: true,
},
"id": schema.StringAttribute{
Description: "The ID of the integration.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"name": schema.StringAttribute{
Description: "The name of the integration.",
Required: true,
},
},
}
}

func (r *IntegrationAzureResourcesResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var planModel IntegrationAzureResourcesResourceModel

resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}

input := opslevel.AzureResourcesIntegrationInput{
Name: planModel.Name.ValueStringPointer(),
TenantId: planModel.TenantId.ValueStringPointer(),
SubscriptionId: planModel.SubscriptionId.ValueStringPointer(),
ClientId: planModel.ClientId.ValueStringPointer(),
ClientSecret: planModel.ClientSecret.ValueStringPointer(),
}

azureResourcesIntegration, err := r.client.CreateIntegrationAzureResources(input)
if err != nil {
resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to create Azure Resources integration, got error: '%s'", err))
return
}

stateModel, diags := NewIntegrationAzureResourcesResourceModel(ctx, *azureResourcesIntegration, planModel)
if diags != nil && diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}

tflog.Trace(ctx, "created an Azure Resources integration")
resp.Diagnostics.Append(resp.State.Set(ctx, &stateModel)...)
}

func (r *IntegrationAzureResourcesResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var stateModel IntegrationAzureResourcesResourceModel

resp.Diagnostics.Append(req.State.Get(ctx, &stateModel)...)
if resp.Diagnostics.HasError() {
return
}

azureResourcesIntegration, err := r.client.GetIntegration(opslevel.ID(stateModel.Id.ValueString()))
if err != nil {
resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to read Azure Resources integration, got error: '%s'", err))
return
}

verifiedStateModel, diags := NewIntegrationAzureResourcesResourceModel(ctx, *azureResourcesIntegration, stateModel)
if diags != nil && diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}

// Save updated data into Terraform state
tflog.Trace(ctx, "read an Azure Resources integration")
resp.Diagnostics.Append(resp.State.Set(ctx, &verifiedStateModel)...)
}

func (r *IntegrationAzureResourcesResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var planModel IntegrationAzureResourcesResourceModel

resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}

input := opslevel.AzureResourcesIntegrationInput{
Name: planModel.Name.ValueStringPointer(),
TenantId: planModel.TenantId.ValueStringPointer(),
SubscriptionId: planModel.SubscriptionId.ValueStringPointer(),
ClientId: planModel.ClientId.ValueStringPointer(),
ClientSecret: planModel.ClientSecret.ValueStringPointer(),
}

azureResourcesIntegration, err := r.client.UpdateIntegrationAzureResources(planModel.Id.ValueString(), input)
if err != nil {
resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to update Azure Resources integration, got error: '%s'", err))
return
}

stateModel, diags := NewIntegrationAzureResourcesResourceModel(ctx, *azureResourcesIntegration, planModel)
if diags != nil && diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}

tflog.Trace(ctx, "updated an Azure Resources integration")
resp.Diagnostics.Append(resp.State.Set(ctx, &stateModel)...)
}

func (r *IntegrationAzureResourcesResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data IntegrationAzureResourcesResourceModel

resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

if err := r.client.DeleteIntegration(data.Id.ValueString()); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete Azure Resources integration, got error: '%s'", err))
return
}
tflog.Trace(ctx, "deleted an Azure Resources integration")
}

func (r *IntegrationAzureResourcesResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
mock_resource "opslevel_integration_azure_resources" {
defaults = {
aliases = ["alias1", "alias2"]
client_id = "XXX_CLIENT_ID_XXX"
client_secret = "XXX_CLIENT_SECRET_XXX"
last_synced_at = "2024-07-08T13:50:07Z"
}
}

51 changes: 51 additions & 0 deletions tests/local/resource_integration_azure_resources.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
mock_provider "opslevel" {
alias = "fake"
source = "./mock_resource"
}

run "resource_infra_azure_resources_small" {
providers = {
opslevel = opslevel.fake
}

assert {
condition = opslevel_integration_azure_resources.example.last_synced_at == "2024-07-08T13:50:07Z"
error_message = "wrong last_synced_at for opslevel_integration_azure_resources.example"
}

assert {
condition = can(opslevel_integration_azure_resources.example.id)
error_message = "expected opslevel_integration_azure_resources to have an ID"
}

assert {
condition = opslevel_integration_azure_resources.example.name == "dev"
error_message = "wrong name for opslevel_integration_azure_resources.example"
}

assert {
condition = opslevel_integration_azure_resources.example.tenant_id == "XXX_TENANT_ID_XXX"
error_message = "wrong tenant_id for opslevel_integration_azure_resources.example"
}

assert {
condition = opslevel_integration_azure_resources.example.subscription_id == "XXX_SUBSCRIPTION_ID_XXX"
error_message = "wrong subscription_id for opslevel_integration_azure_resources.example"
}

assert {
condition = opslevel_integration_azure_resources.example.client_id == "XXX_CLIENT_ID_XXX"
error_message = "wrong client_id for opslevel_integration_azure_resources.example"
}

assert {
condition = opslevel_integration_azure_resources.example.client_secret == "XXX_CLIENT_SECRET_XXX"
error_message = "wrong client_secret for opslevel_integration_azure_resources.example"
}

assert {
condition = opslevel_integration_azure_resources.example.aliases == tolist(["alias1", "alias2"])
error_message = "wrong aliases for opslevel_integration_azure_resources.example"
}

}
10 changes: 10 additions & 0 deletions tests/local/resources.tf
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ resource "opslevel_integration_aws" "example" {
ownership_tag_keys = ["owner", "team", "group"]
}

# Integration Azure Resources resources

resource "opslevel_integration_azure_resources" "example" {
client_id = "XXX_CLIENT_ID_XXX"
client_secret = "XXX_CLIENT_SECRET_XXX"
name = "dev"
subscription_id = "XXX_SUBSCRIPTION_ID_XXX"
tenant_id = "XXX_TENANT_ID_XXX"
}

# Property Assignment

resource "opslevel_property_assignment" "color_picker_using_aliases" {
Expand Down
Loading