Skip to content

Commit d7a7c2c

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

13 files changed

+756
-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: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
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(
123+
regexp.MustCompile(`^$|^([a-zA-Z0-9_-]+:\S+)(\s[a-zA-Z0-9_-]+:\S+)*$`),
124+
"Scope query must be a valid Datadog query (e.g. tier:critical team:team-a)",
125+
),
126+
},
127+
PlanModifiers: []planmodifier.String{
128+
stringplanmodifier.UseStateForUnknown(),
129+
},
130+
},
131+
"created_at": schema.StringAttribute{
132+
Description: "Creation time of the rule",
133+
Computed: true,
134+
},
135+
"modified_at": schema.StringAttribute{
136+
Description: "Last modification time of the rule",
137+
Computed: true,
138+
},
139+
},
140+
}
141+
}
142+
143+
func (r *scorecardRuleResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) {
144+
resource.ImportStatePassthroughID(ctx, frameworkPath.Root("id"), request, response)
145+
}
146+
147+
func (r *scorecardRuleResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
148+
var state scorecardRuleModel
149+
response.Diagnostics.Append(request.Plan.Get(ctx, &state)...)
150+
if response.Diagnostics.HasError() {
151+
return
152+
}
153+
attrs, diags := r.buildServiceScorecardRule(ctx, &state)
154+
response.Diagnostics.Append(diags...)
155+
if response.Diagnostics.HasError() {
156+
return
157+
}
158+
159+
ruleReq := datadogV2.NewCreateRuleRequest()
160+
ruleReq.SetData(*datadogV2.NewCreateRuleRequestData())
161+
ruleReq.Data.SetAttributes(*attrs)
162+
163+
res, _, err := r.Api.CreateScorecardRule(r.Auth, *ruleReq)
164+
if err != nil {
165+
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating Scorecard Rule"))
166+
return
167+
}
168+
if err := utils.CheckForUnparsed(res); err != nil {
169+
response.Diagnostics.AddError("response contains unparsedObject", err.Error())
170+
return
171+
}
172+
173+
state.ID = types.StringValue(res.Data.GetId())
174+
response.Diagnostics.Append(r.updateState(ctx, &state, res.Data.Attributes)...)
175+
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
176+
}
177+
178+
func (r *scorecardRuleResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
179+
var state scorecardRuleModel
180+
response.Diagnostics.Append(request.State.Get(ctx, &state)...)
181+
if response.Diagnostics.HasError() {
182+
return
183+
}
184+
185+
optParams := datadogV2.NewListScorecardRulesOptionalParameters().WithFilterRuleId(state.ID.ValueString())
186+
res, _, err := r.Api.ListScorecardRules(r.Auth, *optParams)
187+
if err != nil {
188+
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error retrieving Scorecard Rule"))
189+
return
190+
}
191+
if err := utils.CheckForUnparsed(res); err != nil {
192+
response.Diagnostics.AddError("response contains unparsedObject", err.Error())
193+
return
194+
}
195+
196+
if err := utils.CheckForUnparsed(res); err != nil {
197+
response.Diagnostics.AddError("response contains unparsedObject", err.Error())
198+
return
199+
}
200+
if len(res.Data) < 1 {
201+
response.Diagnostics.AddError(
202+
fmt.Sprintf("Scorecard Rule with ID %s not found", state.ID.ValueString()),
203+
"No rule was returned by the API.",
204+
)
205+
return
206+
}
207+
response.Diagnostics.Append(r.updateState(ctx, &state, res.Data[0].Attributes)...)
208+
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
209+
}
210+
211+
func (r *scorecardRuleResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
212+
var state scorecardRuleModel
213+
response.Diagnostics.Append(request.Plan.Get(ctx, &state)...)
214+
if response.Diagnostics.HasError() {
215+
return
216+
}
217+
218+
attrs, diags := r.buildServiceScorecardRule(ctx, &state)
219+
response.Diagnostics.Append(diags...)
220+
if response.Diagnostics.HasError() {
221+
return
222+
}
223+
224+
ruleReq := datadogV2.NewUpdateRuleRequest()
225+
ruleReq.SetData(*datadogV2.NewUpdateRuleRequestData())
226+
ruleReq.Data.SetAttributes(*attrs)
227+
228+
res, _, err := r.Api.UpdateScorecardRule(r.Auth, state.ID.ValueString(), *ruleReq)
229+
if err != nil {
230+
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating Scorecard Rule"))
231+
return
232+
}
233+
if err := utils.CheckForUnparsed(res); err != nil {
234+
response.Diagnostics.AddError("response contains unparsedObject", err.Error())
235+
return
236+
}
237+
238+
response.Diagnostics.Append(r.updateState(ctx, &state, res.Data.Attributes)...)
239+
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
240+
}
241+
242+
func (r *scorecardRuleResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
243+
var state scorecardRuleModel
244+
response.Diagnostics.Append(request.State.Get(ctx, &state)...)
245+
if response.Diagnostics.HasError() {
246+
return
247+
}
248+
249+
httpResp, err := r.Api.DeleteScorecardRule(r.Auth, state.ID.ValueString())
250+
if err != nil {
251+
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error deleting Scorecard Rule"))
252+
return
253+
}
254+
if httpResp.StatusCode == 404 {
255+
return // already deleted
256+
}
257+
}
258+
259+
func (r *scorecardRuleResource) buildServiceScorecardRule(ctx context.Context, state *scorecardRuleModel) (*datadogV2.RuleAttributes, diag.Diagnostics) {
260+
diags := diag.Diagnostics{}
261+
262+
attrs := datadogV2.RuleAttributes{}
263+
if attrs.AdditionalProperties == nil {
264+
attrs.AdditionalProperties = make(map[string]interface{})
265+
}
266+
267+
attrs.SetName(state.Name.ValueString())
268+
attrs.SetEnabled(state.Enabled.ValueBool())
269+
attrs.SetOwner(state.Owner.ValueString())
270+
attrs.SetScorecardName(state.ScorecardName.ValueString())
271+
attrs.AdditionalProperties["level"] = state.Level.ValueInt32()
272+
273+
if !state.Description.IsNull() {
274+
attrs.SetDescription(state.Description.ValueString())
275+
}
276+
277+
if !state.ScopeQuery.IsNull() {
278+
attrs.AdditionalProperties["scope_query"] = state.ScopeQuery.ValueString()
279+
}
280+
281+
return &attrs, diags
282+
}
283+
284+
func (r *scorecardRuleResource) updateState(ctx context.Context, state *scorecardRuleModel, attrs *datadogV2.RuleAttributes) diag.Diagnostics {
285+
diags := diag.Diagnostics{}
286+
287+
if createdAt, ok := attrs.GetCreatedAtOk(); ok {
288+
state.CreatedAt = types.StringValue(createdAt.Format(time.RFC3339Nano))
289+
}
290+
291+
if modifiedAt, ok := attrs.GetModifiedAtOk(); ok {
292+
state.ModifiedAt = types.StringValue(modifiedAt.Format(time.RFC3339Nano))
293+
}
294+
295+
state.Name = types.StringPointerValue(attrs.Name)
296+
state.ScorecardName = types.StringPointerValue(attrs.ScorecardName)
297+
state.Enabled = types.BoolPointerValue(attrs.Enabled)
298+
299+
if owner, ok := attrs.GetOwnerOk(); ok {
300+
state.Owner = types.StringPointerValue(owner)
301+
}
302+
303+
if desc, ok := attrs.GetDescriptionOk(); ok {
304+
state.Description = types.StringPointerValue(desc)
305+
}
306+
307+
if custom, ok := attrs.GetCustomOk(); ok {
308+
state.Custom = types.BoolPointerValue(custom)
309+
}
310+
311+
if sq, ok := attrs.AdditionalProperties["scope_query"].(string); ok {
312+
state.ScopeQuery = types.StringValue(sq)
313+
}
314+
315+
if level, ok := attrs.AdditionalProperties["level"].(int32); ok {
316+
state.Level = types.Int32Value(level)
317+
}
318+
319+
return diags
320+
}

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)