Skip to content

Commit 1791538

Browse files
Brian Kanebriankane
authored andcommitted
feature: Adds Service Scorecard Resource
Signed-off-by: Brian Kane <[email protected]>
1 parent 9ca1d0d commit 1791538

13 files changed

+754
-0
lines changed

datadog/fwprovider/framework_provider.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ var Resources = []func() resource.Resource{
9292
NewCostBudgetResource,
9393
NewCSMThreatsAgentRuleResource,
9494
NewCSMThreatsPolicyResource,
95+
NewScorecardRuleResource,
9596
}
9697

9798
var Datasources = []func() datasource.DataSource{
@@ -455,6 +456,12 @@ func defaultConfigureFunc(p *FrameworkProvider, request *provider.ConfigureReque
455456
ddClientConfig.SetUnstableOperationEnabled("v2.DeleteMonitorNotificationRule", true)
456457
ddClientConfig.SetUnstableOperationEnabled("v2.UpdateMonitorNotificationRule", true)
457458

459+
// Enable ServiceScorecards
460+
ddClientConfig.SetUnstableOperationEnabled("v2.CreateScorecardRule", true)
461+
ddClientConfig.SetUnstableOperationEnabled("v2.UpdateScorecardRule", true)
462+
ddClientConfig.SetUnstableOperationEnabled("v2.ListScorecardRules", true)
463+
ddClientConfig.SetUnstableOperationEnabled("v2.DeleteScorecardRule", true)
464+
458465
if !config.ApiUrl.IsNull() && config.ApiUrl.ValueString() != "" {
459466
parsedAPIURL, parseErr := url.Parse(config.ApiUrl.ValueString())
460467
if parseErr != nil {
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
package fwprovider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"regexp"
7+
"time"
8+
9+
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"
10+
"github.com/hashicorp/terraform-plugin-framework-validators/int32validator"
11+
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
12+
"github.com/hashicorp/terraform-plugin-framework/diag"
13+
frameworkPath "github.com/hashicorp/terraform-plugin-framework/path"
14+
"github.com/hashicorp/terraform-plugin-framework/resource"
15+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
16+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
17+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
18+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int32default"
19+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier"
20+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
21+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
22+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
23+
"github.com/hashicorp/terraform-plugin-framework/types"
24+
25+
"github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils"
26+
)
27+
28+
var (
29+
_ resource.ResourceWithConfigure = &scorecardRuleResource{}
30+
_ resource.ResourceWithImportState = &scorecardRuleResource{}
31+
)
32+
33+
type scorecardRuleResource struct {
34+
Api *datadogV2.ServiceScorecardsApi
35+
Auth context.Context
36+
}
37+
38+
type scorecardRuleModel struct {
39+
CreatedAt types.String `tfsdk:"created_at"`
40+
ModifiedAt types.String `tfsdk:"modified_at"`
41+
ID types.String `tfsdk:"id"`
42+
Name types.String `tfsdk:"name"`
43+
Description types.String `tfsdk:"description"`
44+
Custom types.Bool `tfsdk:"custom"`
45+
Enabled types.Bool `tfsdk:"enabled"`
46+
Owner types.String `tfsdk:"owner"`
47+
ScorecardName types.String `tfsdk:"scorecard_name"`
48+
Level types.Int32 `tfsdk:"level"`
49+
ScopeQuery types.String `tfsdk:"scope_query"`
50+
}
51+
52+
func NewScorecardRuleResource() resource.Resource {
53+
return &scorecardRuleResource{}
54+
}
55+
56+
func (r *scorecardRuleResource) Configure(_ context.Context, request resource.ConfigureRequest, _ *resource.ConfigureResponse) {
57+
providerData := request.ProviderData.(*FrameworkProvider)
58+
r.Api = providerData.DatadogApiInstances.GetServiceScorecardsApiV2()
59+
r.Auth = providerData.Auth
60+
}
61+
62+
func (r *scorecardRuleResource) Metadata(_ context.Context, _ resource.MetadataRequest, response *resource.MetadataResponse) {
63+
response.TypeName = "service_scorecard_rule"
64+
}
65+
66+
func (r *scorecardRuleResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) {
67+
response.Schema = schema.Schema{
68+
Description: "Provides a Datadog Service Scorecard Rule resource. This can be used to create and manage Datadog Service Scorecard Rules.",
69+
Attributes: map[string]schema.Attribute{
70+
"id": utils.ResourceIDAttribute(),
71+
"name": schema.StringAttribute{
72+
Description: "Name of the rule.",
73+
Required: true,
74+
Validators: []validator.String{
75+
stringvalidator.LengthBetween(1, 140),
76+
},
77+
},
78+
"description": schema.StringAttribute{
79+
Description: "Explanation of the rule.",
80+
Optional: true,
81+
PlanModifiers: []planmodifier.String{
82+
stringplanmodifier.UseStateForUnknown(),
83+
},
84+
},
85+
"enabled": schema.BoolAttribute{
86+
Description: "If enabled, the rule is calculated as part of the score",
87+
Optional: true,
88+
Default: booldefault.StaticBool(true),
89+
Computed: true,
90+
PlanModifiers: []planmodifier.Bool{
91+
boolplanmodifier.UseStateForUnknown(),
92+
},
93+
},
94+
"custom": schema.BoolAttribute{
95+
Description: "Defines if the rule is a custom rule",
96+
Computed: true,
97+
},
98+
"owner": schema.StringAttribute{
99+
Description: "Owner of the rule.",
100+
Optional: true,
101+
},
102+
"scorecard_name": schema.StringAttribute{
103+
Description: "The scorecard name to which this rule must belong",
104+
Required: true,
105+
},
106+
"level": schema.Int32Attribute{
107+
Description: "The criticality level of the rule",
108+
Optional: true,
109+
Default: int32default.StaticInt32(3),
110+
Computed: true,
111+
Validators: []validator.Int32{
112+
int32validator.OneOf(int32(1), int32(2), int32(3)),
113+
},
114+
PlanModifiers: []planmodifier.Int32{
115+
int32planmodifier.UseStateForUnknown(),
116+
},
117+
},
118+
"scope_query": schema.StringAttribute{
119+
Description: "The scope query to apply to the rule",
120+
Optional: true,
121+
Validators: []validator.String{
122+
stringvalidator.RegexMatches(regexp.MustCompile(`^([a-zA-Z0-9_-]+:\S+)(\s[a-zA-Z0-9_-]+:\S+)*$`),
123+
"Scope query must be a valid Datadog query format"),
124+
},
125+
PlanModifiers: []planmodifier.String{
126+
stringplanmodifier.UseStateForUnknown(),
127+
},
128+
},
129+
"created_at": schema.StringAttribute{
130+
Description: "Creation time of the rule",
131+
Computed: true,
132+
},
133+
"modified_at": schema.StringAttribute{
134+
Description: "Last modification time of the rule",
135+
Computed: true,
136+
},
137+
},
138+
}
139+
}
140+
141+
func (r *scorecardRuleResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) {
142+
resource.ImportStatePassthroughID(ctx, frameworkPath.Root("id"), request, response)
143+
}
144+
145+
func (r *scorecardRuleResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
146+
var state scorecardRuleModel
147+
response.Diagnostics.Append(request.Plan.Get(ctx, &state)...)
148+
if response.Diagnostics.HasError() {
149+
return
150+
}
151+
attrs, diags := r.buildServiceScorecardRule(ctx, &state)
152+
response.Diagnostics.Append(diags...)
153+
if response.Diagnostics.HasError() {
154+
return
155+
}
156+
157+
ruleReq := datadogV2.NewCreateRuleRequest()
158+
ruleReq.SetData(*datadogV2.NewCreateRuleRequestData())
159+
ruleReq.Data.SetAttributes(*attrs)
160+
161+
res, _, err := r.Api.CreateScorecardRule(r.Auth, *ruleReq)
162+
if err != nil {
163+
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating Scorecard Rule"))
164+
return
165+
}
166+
if err := utils.CheckForUnparsed(res); err != nil {
167+
response.Diagnostics.AddError("response contains unparsedObject", err.Error())
168+
return
169+
}
170+
171+
state.ID = types.StringValue(res.Data.GetId())
172+
response.Diagnostics.Append(r.updateState(ctx, &state, res.Data.Attributes)...)
173+
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
174+
}
175+
176+
func (r *scorecardRuleResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
177+
var state scorecardRuleModel
178+
response.Diagnostics.Append(request.State.Get(ctx, &state)...)
179+
if response.Diagnostics.HasError() {
180+
return
181+
}
182+
183+
optParams := datadogV2.NewListScorecardRulesOptionalParameters().WithFilterRuleId(state.ID.ValueString())
184+
res, _, err := r.Api.ListScorecardRules(r.Auth, *optParams)
185+
if err != nil {
186+
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error retrieving Scorecard Rule"))
187+
return
188+
}
189+
if err := utils.CheckForUnparsed(res); err != nil {
190+
response.Diagnostics.AddError("response contains unparsedObject", err.Error())
191+
return
192+
}
193+
194+
if err := utils.CheckForUnparsed(res); err != nil {
195+
response.Diagnostics.AddError("response contains unparsedObject", err.Error())
196+
return
197+
}
198+
if len(res.Data) < 1 {
199+
response.Diagnostics.AddError(
200+
fmt.Sprintf("Scorecard Rule with ID %s not found", state.ID.ValueString()),
201+
"No rule was returned by the API.",
202+
)
203+
return
204+
}
205+
response.Diagnostics.Append(r.updateState(ctx, &state, res.Data[0].Attributes)...)
206+
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
207+
}
208+
209+
func (r *scorecardRuleResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
210+
var state scorecardRuleModel
211+
response.Diagnostics.Append(request.Plan.Get(ctx, &state)...)
212+
if response.Diagnostics.HasError() {
213+
return
214+
}
215+
216+
attrs, diags := r.buildServiceScorecardRule(ctx, &state)
217+
response.Diagnostics.Append(diags...)
218+
if response.Diagnostics.HasError() {
219+
return
220+
}
221+
222+
ruleReq := datadogV2.NewUpdateRuleRequest()
223+
ruleReq.SetData(*datadogV2.NewUpdateRuleRequestData())
224+
ruleReq.Data.SetAttributes(*attrs)
225+
226+
res, _, err := r.Api.UpdateScorecardRule(r.Auth, state.ID.ValueString(), *ruleReq)
227+
if err != nil {
228+
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating Scorecard Rule"))
229+
return
230+
}
231+
if err := utils.CheckForUnparsed(res); err != nil {
232+
response.Diagnostics.AddError("response contains unparsedObject", err.Error())
233+
return
234+
}
235+
236+
response.Diagnostics.Append(r.updateState(ctx, &state, res.Data.Attributes)...)
237+
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
238+
}
239+
240+
func (r *scorecardRuleResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
241+
var state scorecardRuleModel
242+
response.Diagnostics.Append(request.State.Get(ctx, &state)...)
243+
if response.Diagnostics.HasError() {
244+
return
245+
}
246+
247+
httpResp, err := r.Api.DeleteScorecardRule(r.Auth, state.ID.ValueString())
248+
if err != nil {
249+
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error deleting Scorecard Rule"))
250+
return
251+
}
252+
if httpResp.StatusCode == 404 {
253+
return // already deleted
254+
}
255+
}
256+
257+
func (r *scorecardRuleResource) buildServiceScorecardRule(ctx context.Context, state *scorecardRuleModel) (*datadogV2.RuleAttributes, diag.Diagnostics) {
258+
diags := diag.Diagnostics{}
259+
260+
attrs := datadogV2.RuleAttributes{}
261+
if attrs.AdditionalProperties == nil {
262+
attrs.AdditionalProperties = make(map[string]interface{})
263+
}
264+
265+
attrs.SetName(state.Name.ValueString())
266+
attrs.SetEnabled(state.Enabled.ValueBool())
267+
attrs.SetOwner(state.Owner.ValueString())
268+
attrs.SetScorecardName(state.ScorecardName.ValueString())
269+
attrs.AdditionalProperties["level"] = state.Level.ValueInt32()
270+
271+
if !state.Description.IsNull() {
272+
attrs.SetDescription(state.Description.ValueString())
273+
}
274+
275+
if !state.ScopeQuery.IsNull() {
276+
attrs.AdditionalProperties["scope_query"] = state.ScopeQuery.ValueString()
277+
}
278+
279+
return &attrs, diags
280+
}
281+
282+
func (r *scorecardRuleResource) updateState(ctx context.Context, state *scorecardRuleModel, attrs *datadogV2.RuleAttributes) diag.Diagnostics {
283+
diags := diag.Diagnostics{}
284+
285+
if createdAt, ok := attrs.GetCreatedAtOk(); ok {
286+
state.CreatedAt = types.StringValue(createdAt.Format(time.RFC3339Nano))
287+
}
288+
289+
if modifiedAt, ok := attrs.GetModifiedAtOk(); ok {
290+
state.ModifiedAt = types.StringValue(modifiedAt.Format(time.RFC3339Nano))
291+
}
292+
293+
state.Name = types.StringPointerValue(attrs.Name)
294+
state.ScorecardName = types.StringPointerValue(attrs.ScorecardName)
295+
state.Enabled = types.BoolPointerValue(attrs.Enabled)
296+
297+
if owner, ok := attrs.GetOwnerOk(); ok {
298+
state.Owner = types.StringPointerValue(owner)
299+
}
300+
301+
if desc, ok := attrs.GetDescriptionOk(); ok {
302+
state.Description = types.StringPointerValue(desc)
303+
}
304+
305+
if custom, ok := attrs.GetCustomOk(); ok {
306+
state.Custom = types.BoolPointerValue(custom)
307+
}
308+
309+
if sq, ok := attrs.AdditionalProperties["scope_query"].(string); ok {
310+
state.ScopeQuery = types.StringValue(sq)
311+
}
312+
313+
if level, ok := attrs.AdditionalProperties["level"].(int32); ok {
314+
state.Level = types.Int32Value(level)
315+
}
316+
317+
return diags
318+
}

datadog/internal/utils/api_instances_helper.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ type ApiInstances struct {
8888
securityMonitoringApiV2 *datadogV2.SecurityMonitoringApi
8989
sensitiveDataScannerApiV2 *datadogV2.SensitiveDataScannerApi
9090
serviceAccountsApiV2 *datadogV2.ServiceAccountsApi
91+
serviceScorecardsApiV2 *datadogV2.ServiceScorecardsApi
9192
softwareCatalogApiV2 *datadogV2.SoftwareCatalogApi
9293
spansMetricsApiV2 *datadogV2.SpansMetricsApi
9394
syntheticsApiV2 *datadogV2.SyntheticsApi
@@ -616,6 +617,14 @@ func (i *ApiInstances) GetServiceAccountsApiV2() *datadogV2.ServiceAccountsApi {
616617
return i.serviceAccountsApiV2
617618
}
618619

620+
// GetServiceScorecardsApiV2 get instance of ServiceScorecardsApi
621+
func (i *ApiInstances) GetServiceScorecardsApiV2() *datadogV2.ServiceScorecardsApi {
622+
if i.serviceScorecardsApiV2 == nil {
623+
i.serviceScorecardsApiV2 = datadogV2.NewServiceScorecardsApi(i.HttpClient)
624+
}
625+
return i.serviceScorecardsApiV2
626+
}
627+
619628
// GetSoftwareCatalogApiV2 get instance of SoftwareCatalogApi
620629
func (i *ApiInstances) GetSoftwareCatalogApiV2() *datadogV2.SoftwareCatalogApi {
621630
if i.softwareCatalogApiV2 == nil {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2025-07-01T08:46:03.603664+01:00

0 commit comments

Comments
 (0)