From 87183cbec8e9bbb5346b16632a13fcf19e7ac78e Mon Sep 17 00:00:00 2001 From: David Bloss Date: Fri, 9 Aug 2024 10:51:33 -0700 Subject: [PATCH] add opslevel_service_dependencies datasource (#429) * add opslevel_service_dependencies datasource * add dependents to service dependencies datasource * Apply suggestions from code review Co-authored-by: Taimoor Ahmad * code cleanup and azure fix from upstream update * add example for opslevel_service_dependencies datasource --------- Co-authored-by: Taimoor Ahmad --- .../unreleased/Feature-20240808-161107.yaml | 3 + .../data-source.tf | 15 ++ ...atasource_opslevel_service_dependencies.go | 175 ++++++++++++++++++ opslevel/provider.go | 1 + ...ce_opslevel_integration_azure_resources.go | 4 +- submodules/opslevel-go | 2 +- 6 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 .changes/unreleased/Feature-20240808-161107.yaml create mode 100644 examples/data-sources/opslevel_service_dependencies/data-source.tf create mode 100644 opslevel/datasource_opslevel_service_dependencies.go diff --git a/.changes/unreleased/Feature-20240808-161107.yaml b/.changes/unreleased/Feature-20240808-161107.yaml new file mode 100644 index 00000000..a4ce6b7f --- /dev/null +++ b/.changes/unreleased/Feature-20240808-161107.yaml @@ -0,0 +1,3 @@ +kind: Feature +body: add opslevel_service_dependencies datasource +time: 2024-08-08T16:11:07.119526-05:00 diff --git a/examples/data-sources/opslevel_service_dependencies/data-source.tf b/examples/data-sources/opslevel_service_dependencies/data-source.tf new file mode 100644 index 00000000..13ba328b --- /dev/null +++ b/examples/data-sources/opslevel_service_dependencies/data-source.tf @@ -0,0 +1,15 @@ +data "opslevel_service" "foo" { + alias = "foo" +} + +data "opslevel_service" "bar" { + id = "Z2lkOi8vb3BzbGV2ZWwvU2VydmljZS84Njcw" +} + +data "opslevel_service_dependencies" "by_alias" { + service = data.opslevel_service.foo.alias +} + +data "opslevel_service_dependencies" "by_id" { + service = data.opslevel_service.bar.id +} diff --git a/opslevel/datasource_opslevel_service_dependencies.go b/opslevel/datasource_opslevel_service_dependencies.go new file mode 100644 index 00000000..12149b61 --- /dev/null +++ b/opslevel/datasource_opslevel_service_dependencies.go @@ -0,0 +1,175 @@ +package opslevel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/opslevel/opslevel-go/v2024" +) + +// Ensure ServiceDataSource implements DataSourceWithConfigure interface +var _ datasource.DataSourceWithConfigure = &ServiceDataSource{} + +func NewServiceDependenciesDataSource() datasource.DataSource { + return &ServiceDependenciesDataSource{} +} + +// ServiceDataSource manages a Service data source. +type ServiceDependenciesDataSource struct { + CommonDataSourceClient +} + +// ServiceDependenciesModel describes the data source data model. +type ServiceDependenciesModel struct { + Dependents []dependentsModel `tfsdk:"dependents"` + Dependencies []dependenciesModel `tfsdk:"dependencies"` + Service types.String `tfsdk:"service"` +} + +type dependentsModel struct { + Id types.String `tfsdk:"id"` + Locked types.Bool `tfsdk:"locked"` + Notes types.String `tfsdk:"notes"` +} + +type dependenciesModel struct { + Id types.String `tfsdk:"id"` + Locked types.Bool `tfsdk:"locked"` + Notes types.String `tfsdk:"notes"` +} + +func NewServiceDependenciesModel(serviceIdentifier string, dependents []dependentsModel, dependencies []dependenciesModel) ServiceDependenciesModel { + return ServiceDependenciesModel{ + Dependents: dependents, + Dependencies: dependencies, + Service: types.StringValue(serviceIdentifier), + } +} + +func (d *ServiceDependenciesDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_service_dependencies" +} + +var depsAttrs = map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the service dependency.", + Computed: true, + }, + "locked": schema.BoolAttribute{ + Description: "Is the dependency locked by a service config?", + Computed: true, + }, + "notes": schema.StringAttribute{ + Description: "Notes for service dependency.", + Optional: true, + }, +} + +func (d *ServiceDependenciesDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Service Dependencies data source", + + Attributes: map[string]schema.Attribute{ + "dependents": schema.ListNestedAttribute{ + Description: "List of Service Dependents of a service", + NestedObject: schema.NestedAttributeObject{ + Attributes: depsAttrs, + }, + Computed: true, + }, + "dependencies": schema.ListNestedAttribute{ + Description: "List of Service Dependencies of a service", + NestedObject: schema.NestedAttributeObject{ + Attributes: depsAttrs, + }, + Computed: true, + }, + "service": schema.StringAttribute{ + Description: "The ID or alias of the service with the dependency.", + Required: true, + }, + }, + } +} + +func (d *ServiceDependenciesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var ( + err error + service *opslevel.Service + serviceIdentifier string + ) + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("service"), &serviceIdentifier)...) + if resp.Diagnostics.HasError() { + return + } + + // Retrieve Service + if opslevel.IsID(serviceIdentifier) { + service, err = d.client.GetService(opslevel.ID(serviceIdentifier)) + } else { + service, err = d.client.GetServiceWithAlias(serviceIdentifier) + } + if err != nil || service == nil { + resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to read service, got error: %s", err)) + return + } + + dependenciesModel, err := getDependenciesModelOfService(d.client, service) + if err != nil { + resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to get dependencies for service, got error: %s", err)) + return + } + + dependentsModel, err := getDependentsModelOfService(d.client, service) + if err != nil { + resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to get dependents for service, got error: %s", err)) + return + } + + stateModel := NewServiceDependenciesModel(serviceIdentifier, dependentsModel, dependenciesModel) + + // Save data into Terraform state + tflog.Trace(ctx, "read an OpsLevel Service Dependencies data source") + resp.Diagnostics.Append(resp.State.Set(ctx, &stateModel)...) +} + +func getDependenciesModelOfService(client *opslevel.Client, service *opslevel.Service) ([]dependenciesModel, error) { + dependencies := []dependenciesModel{} + svcDependencies, err := service.GetDependencies(client, nil) + if err != nil || svcDependencies == nil { + return dependencies, err + } + + for _, svcDependency := range svcDependencies.Edges { + dependencies = append(dependencies, dependenciesModel{ + Locked: types.BoolValue(svcDependency.Locked), + Id: ComputedStringValue(string(svcDependency.Id)), + Notes: ComputedStringValue(svcDependency.Notes), + }) + } + return dependencies, nil +} + +func getDependentsModelOfService(client *opslevel.Client, service *opslevel.Service) ([]dependentsModel, error) { + dependents := []dependentsModel{} + svcDependents, err := service.GetDependents(client, nil) + if err != nil || svcDependents == nil { + return dependents, err + } + + for _, svcDependent := range svcDependents.Edges { + dependents = append(dependents, dependentsModel{ + Locked: types.BoolValue(svcDependent.Locked), + Id: ComputedStringValue(string(svcDependent.Id)), + Notes: ComputedStringValue(svcDependent.Notes), + }) + } + return dependents, nil +} diff --git a/opslevel/provider.go b/opslevel/provider.go index a5b7e554..74925e29 100644 --- a/opslevel/provider.go +++ b/opslevel/provider.go @@ -239,6 +239,7 @@ func (p *OpslevelProvider) DataSources(context.Context) []func() datasource.Data NewScorecardDataSource, NewScorecardDataSourcesAll, NewServiceDataSource, + NewServiceDependenciesDataSource, NewServiceDataSourcesAll, NewSystemDataSource, NewSystemDataSourcesAll, diff --git a/opslevel/resource_opslevel_integration_azure_resources.go b/opslevel/resource_opslevel_integration_azure_resources.go index 5e45966e..38f3dc46 100644 --- a/opslevel/resource_opslevel_integration_azure_resources.go +++ b/opslevel/resource_opslevel_integration_azure_resources.go @@ -50,7 +50,7 @@ type IntegrationAzureResourcesResourceModel struct { func NewIntegrationAzureResourcesResourceModel(ctx context.Context, azureResourcesIntegration opslevel.Integration, givenModel IntegrationAzureResourcesResourceModel) IntegrationAzureResourcesResourceModel { resourceModel := IntegrationAzureResourcesResourceModel{ - Aliases: OptionalStringListValue(azureResourcesIntegration.Aliases), + Aliases: OptionalStringListValue(azureResourcesIntegration.AzureResourcesIntegrationFragment.Aliases), ClientId: givenModel.ClientId, ClientSecret: givenModel.ClientSecret, CreatedAt: ComputedStringValue(azureResourcesIntegration.CreatedAt.Local().Format(time.RFC850)), @@ -68,7 +68,7 @@ func NewIntegrationAzureResourcesResourceModel(ctx context.Context, azureResourc if givenModel.TagsOverrideOwnership.IsNull() { resourceModel.TagsOverrideOwnership = types.BoolNull() } else { - resourceModel.TagsOverrideOwnership = types.BoolValue(azureResourcesIntegration.TagsOverrideOwnership) + resourceModel.TagsOverrideOwnership = types.BoolValue(azureResourcesIntegration.AzureResourcesIntegrationFragment.TagsOverrideOwnership) } return resourceModel diff --git a/submodules/opslevel-go b/submodules/opslevel-go index d75138ca..ff59e4fc 160000 --- a/submodules/opslevel-go +++ b/submodules/opslevel-go @@ -1 +1 @@ -Subproject commit d75138cae18e055e0f184d30b60b9c6f3a8fc63f +Subproject commit ff59e4fccc0193d40ea8a9e17ab8c917a080ca22