Skip to content

Commit 34625de

Browse files
[datadog_agentless_scanning_gcp_scan_options] Add Terraform provider for GCP scan options (#3321)
feat(agentless): implement gcp scan options tf provider add(agentless): register the new resource in fwprovider/framework_provider fix(agentless): fix gcp project id validation test(agentless): test gcp scan options tf provider docs(agentless): generate docs test(agentless): generate cassettes docs(agentless): fix gcp nutshell perf(agentless): replace listScanOptions with getScanOptions style(agentless): update the gcp project id field description Co-authored-by: DeForest Richards <[email protected]> docs(agentless-gcp): regenerate docs test(agentless-gcp): regenerate cassettes test(agentless-gcp): regenerate cassettes for TestAccDatadogAgentlessScanningGcpScanOptions_InvalidProjectID Co-authored-by: mohamed.challal <[email protected]>
1 parent 1b21901 commit 34625de

15 files changed

+1131
-0
lines changed

datadog/fwprovider/framework_provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ var _ provider.Provider = &FrameworkProvider{}
3232

3333
var Resources = []func() resource.Resource{
3434
NewAgentlessScanningAwsScanOptionsResource,
35+
NewAgentlessScanningGcpScanOptionsResource,
3536
NewOpenapiApiResource,
3637
NewAPIKeyResource,
3738
NewApplicationKeyResource,
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package fwprovider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"regexp"
7+
8+
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"
9+
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
10+
"github.com/hashicorp/terraform-plugin-framework/diag"
11+
"github.com/hashicorp/terraform-plugin-framework/path"
12+
"github.com/hashicorp/terraform-plugin-framework/resource"
13+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
14+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
15+
"github.com/hashicorp/terraform-plugin-framework/types"
16+
17+
"github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils"
18+
)
19+
20+
var (
21+
_ resource.ResourceWithConfigure = &agentlessScanningGcpScanOptionsResource{}
22+
)
23+
24+
type agentlessScanningGcpScanOptionsResource struct {
25+
Api *datadogV2.AgentlessScanningApi
26+
Auth context.Context
27+
}
28+
29+
type agentlessScanningGcpScanOptionsResourceModel struct {
30+
ID types.String `tfsdk:"id"`
31+
GcpProjectId types.String `tfsdk:"gcp_project_id"`
32+
VulnContainersOs types.Bool `tfsdk:"vuln_containers_os"`
33+
VulnHostOs types.Bool `tfsdk:"vuln_host_os"`
34+
}
35+
36+
func NewAgentlessScanningGcpScanOptionsResource() resource.Resource {
37+
return &agentlessScanningGcpScanOptionsResource{}
38+
}
39+
40+
func (r *agentlessScanningGcpScanOptionsResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) {
41+
providerData := request.ProviderData.(*FrameworkProvider)
42+
r.Api = providerData.DatadogApiInstances.GetAgentlessScanningApiV2()
43+
r.Auth = providerData.Auth
44+
}
45+
46+
func (r *agentlessScanningGcpScanOptionsResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) {
47+
response.TypeName = "agentless_scanning_gcp_scan_options"
48+
}
49+
50+
func (r *agentlessScanningGcpScanOptionsResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) {
51+
response.Schema = schema.Schema{
52+
Description: "Provides a Datadog Agentless Scanning GCP scan options resource. This can be used to activate and configure Agentless scan options for a GCP project.",
53+
Attributes: map[string]schema.Attribute{
54+
// Resource ID
55+
"id": utils.ResourceIDAttribute(),
56+
"gcp_project_id": schema.StringAttribute{
57+
Description: "The GCP project ID for which agentless scanning is configured.",
58+
Required: true,
59+
Validators: []validator.String{
60+
stringvalidator.RegexMatches(
61+
regexp.MustCompile(`^[a-z]([a-z0-9-]{4,28}[a-z0-9])?$`),
62+
"must be a valid GCP project ID: 6–30 characters, start with a lowercase letter, and include only lowercase letters, digits, or hyphens.",
63+
),
64+
},
65+
},
66+
"vuln_containers_os": schema.BoolAttribute{
67+
Description: "Indicates if scanning for vulnerabilities in containers is enabled.",
68+
Required: true,
69+
},
70+
"vuln_host_os": schema.BoolAttribute{
71+
Description: "Indicates if scanning for vulnerabilities in hosts is enabled.",
72+
Required: true,
73+
},
74+
},
75+
}
76+
}
77+
78+
func (r *agentlessScanningGcpScanOptionsResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
79+
var state agentlessScanningGcpScanOptionsResourceModel
80+
response.Diagnostics.Append(request.Plan.Get(ctx, &state)...)
81+
if response.Diagnostics.HasError() {
82+
return
83+
}
84+
85+
body := datadogV2.GcpScanOptions{
86+
Data: &datadogV2.GcpScanOptionsData{
87+
Id: state.GcpProjectId.ValueString(),
88+
Type: datadogV2.GCPSCANOPTIONSDATATYPE_GCP_SCAN_OPTIONS,
89+
Attributes: &datadogV2.GcpScanOptionsDataAttributes{
90+
VulnContainersOs: boolPtr(state.VulnContainersOs.ValueBool()),
91+
VulnHostOs: boolPtr(state.VulnHostOs.ValueBool()),
92+
},
93+
},
94+
}
95+
96+
gcpScanOptionsResponse, _, err := r.Api.CreateGcpScanOptions(r.Auth, body)
97+
if err != nil {
98+
response.Diagnostics.AddError("Error creating GCP scan options", err.Error())
99+
return
100+
}
101+
102+
r.updateStateFromResponse(&state, gcpScanOptionsResponse)
103+
// Set the Terraform resource ID to the GCP project ID
104+
state.ID = types.StringValue(state.GcpProjectId.ValueString())
105+
106+
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
107+
}
108+
109+
func (r *agentlessScanningGcpScanOptionsResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
110+
var state agentlessScanningGcpScanOptionsResourceModel
111+
response.Diagnostics.Append(request.State.Get(ctx, &state)...)
112+
if response.Diagnostics.HasError() {
113+
return
114+
}
115+
116+
projectID := state.GcpProjectId.ValueString()
117+
118+
gcpScanOptionsResponse, _, err := r.Api.GetGcpScanOptions(r.Auth, projectID)
119+
if err != nil {
120+
response.Diagnostics.AddError("Error reading GCP scan options", err.Error())
121+
return
122+
}
123+
124+
r.updateStateFromScanOptionsData(&state, *gcpScanOptionsResponse.Data)
125+
// Set the Terraform resource ID to the GCP project ID
126+
state.ID = types.StringValue(state.GcpProjectId.ValueString())
127+
128+
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
129+
}
130+
131+
func (r *agentlessScanningGcpScanOptionsResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
132+
var state agentlessScanningGcpScanOptionsResourceModel
133+
response.Diagnostics.Append(request.Plan.Get(ctx, &state)...)
134+
if response.Diagnostics.HasError() {
135+
return
136+
}
137+
138+
projectID := state.GcpProjectId.ValueString()
139+
140+
body := datadogV2.GcpScanOptionsInputUpdate{
141+
Data: &datadogV2.GcpScanOptionsInputUpdateData{
142+
Id: state.GcpProjectId.ValueString(),
143+
Type: datadogV2.GCPSCANOPTIONSINPUTUPDATEDATATYPE_GCP_SCAN_OPTIONS,
144+
Attributes: &datadogV2.GcpScanOptionsInputUpdateDataAttributes{
145+
VulnContainersOs: boolPtr(state.VulnContainersOs.ValueBool()),
146+
VulnHostOs: boolPtr(state.VulnHostOs.ValueBool()),
147+
},
148+
},
149+
}
150+
151+
_, res, err := r.Api.UpdateGcpScanOptions(r.Auth, projectID, body)
152+
if err != nil {
153+
errorMsg := "Error updating GCP scan options"
154+
if res != nil {
155+
errorMsg += fmt.Sprintf(". API response: %s", res.Body)
156+
}
157+
response.Diagnostics.AddError(errorMsg, err.Error())
158+
return
159+
}
160+
161+
// After update, we need to read the current state since the API doesn't return the updated object
162+
readReq := resource.ReadRequest{State: response.State}
163+
readResp := resource.ReadResponse{State: response.State, Diagnostics: diag.Diagnostics{}}
164+
165+
// Set the state with current values before reading
166+
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
167+
if response.Diagnostics.HasError() {
168+
return
169+
}
170+
171+
r.Read(ctx, readReq, &readResp)
172+
response.Diagnostics.Append(readResp.Diagnostics...)
173+
response.State = readResp.State
174+
}
175+
176+
func (r *agentlessScanningGcpScanOptionsResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
177+
var state agentlessScanningGcpScanOptionsResourceModel
178+
response.Diagnostics.Append(request.State.Get(ctx, &state)...)
179+
if response.Diagnostics.HasError() {
180+
return
181+
}
182+
183+
projectID := state.GcpProjectId.ValueString()
184+
185+
_, err := r.Api.DeleteGcpScanOptions(r.Auth, projectID)
186+
if err != nil {
187+
response.Diagnostics.AddError("Error deleting GCP scan options", err.Error())
188+
return
189+
}
190+
}
191+
192+
func (r *agentlessScanningGcpScanOptionsResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) {
193+
// Import the GCP project ID as both the Terraform resource ID and the gcp_project_id attribute
194+
resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response)
195+
// Also set the gcp_project_id to the same value
196+
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("gcp_project_id"), request.ID)...)
197+
}
198+
199+
func (r *agentlessScanningGcpScanOptionsResource) updateStateFromResponse(state *agentlessScanningGcpScanOptionsResourceModel, resp datadogV2.GcpScanOptions) {
200+
data := resp.GetData()
201+
r.updateStateFromScanOptionsData(state, data)
202+
}
203+
204+
func (r *agentlessScanningGcpScanOptionsResource) updateStateFromScanOptionsData(state *agentlessScanningGcpScanOptionsResourceModel, data datadogV2.GcpScanOptionsData) {
205+
state.GcpProjectId = types.StringValue(data.GetId())
206+
207+
attributes := data.GetAttributes()
208+
state.VulnContainersOs = types.BoolValue(attributes.GetVulnContainersOs())
209+
state.VulnHostOs = types.BoolValue(attributes.GetVulnHostOs())
210+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2025-11-07T00:42:26.032946+01:00
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
---
2+
version: 2
3+
interactions:
4+
- id: 0
5+
request:
6+
proto: HTTP/1.1
7+
proto_major: 1
8+
proto_minor: 1
9+
content_length: 118
10+
transfer_encoding: []
11+
trailer: {}
12+
host: api.datadoghq.com
13+
remote_addr: ""
14+
request_uri: ""
15+
body: |
16+
{"data":{"attributes":{"vuln_containers_os":true,"vuln_host_os":true},"id":"test-project","type":"gcp_scan_options"}}
17+
form: {}
18+
headers:
19+
Accept:
20+
- application/json
21+
Content-Type:
22+
- application/json
23+
url: https://api.datadoghq.com/api/v2/agentless_scanning/accounts/gcp
24+
method: POST
25+
response:
26+
proto: HTTP/1.1
27+
proto_major: 1
28+
proto_minor: 1
29+
transfer_encoding: []
30+
trailer: {}
31+
content_length: 117
32+
uncompressed: false
33+
body: '{"data":{"id":"test-project","type":"gcp_scan_options","attributes":{"vuln_containers_os":true,"vuln_host_os":true}}}'
34+
headers:
35+
Content-Type:
36+
- application/vnd.api+json
37+
status: 201 Created
38+
code: 201
39+
duration: 839.950875ms
40+
- id: 1
41+
request:
42+
proto: HTTP/1.1
43+
proto_major: 1
44+
proto_minor: 1
45+
content_length: 0
46+
transfer_encoding: []
47+
trailer: {}
48+
host: api.datadoghq.com
49+
remote_addr: ""
50+
request_uri: ""
51+
body: ""
52+
form: {}
53+
headers:
54+
Accept:
55+
- application/json
56+
url: https://api.datadoghq.com/api/v2/agentless_scanning/accounts/gcp
57+
method: GET
58+
response:
59+
proto: HTTP/1.1
60+
proto_major: 1
61+
proto_minor: 1
62+
transfer_encoding: []
63+
trailer: {}
64+
content_length: 119
65+
uncompressed: false
66+
body: '{"data":[{"id":"test-project","type":"gcp_scan_options","attributes":{"vuln_containers_os":true,"vuln_host_os":true}}]}'
67+
headers:
68+
Content-Type:
69+
- application/vnd.api+json
70+
status: 200 OK
71+
code: 200
72+
duration: 259.589417ms
73+
- id: 2
74+
request:
75+
proto: HTTP/1.1
76+
proto_major: 1
77+
proto_minor: 1
78+
content_length: 0
79+
transfer_encoding: []
80+
trailer: {}
81+
host: api.datadoghq.com
82+
remote_addr: ""
83+
request_uri: ""
84+
body: ""
85+
form: {}
86+
headers:
87+
Accept:
88+
- application/json
89+
url: https://api.datadoghq.com/api/v2/agentless_scanning/accounts/gcp/test-project
90+
method: GET
91+
response:
92+
proto: HTTP/1.1
93+
proto_major: 1
94+
proto_minor: 1
95+
transfer_encoding: []
96+
trailer: {}
97+
content_length: 117
98+
uncompressed: false
99+
body: '{"data":{"id":"test-project","type":"gcp_scan_options","attributes":{"vuln_containers_os":true,"vuln_host_os":true}}}'
100+
headers:
101+
Content-Type:
102+
- application/vnd.api+json
103+
status: 200 OK
104+
code: 200
105+
duration: 180.8215ms
106+
- id: 3
107+
request:
108+
proto: HTTP/1.1
109+
proto_major: 1
110+
proto_minor: 1
111+
content_length: 0
112+
transfer_encoding: []
113+
trailer: {}
114+
host: api.datadoghq.com
115+
remote_addr: ""
116+
request_uri: ""
117+
body: ""
118+
form: {}
119+
headers:
120+
Accept:
121+
- '*/*'
122+
url: https://api.datadoghq.com/api/v2/agentless_scanning/accounts/gcp/test-project
123+
method: DELETE
124+
response:
125+
proto: HTTP/1.1
126+
proto_major: 1
127+
proto_minor: 1
128+
transfer_encoding: []
129+
trailer: {}
130+
content_length: 0
131+
uncompressed: false
132+
body: ""
133+
headers: {}
134+
status: 204 No Content
135+
code: 204
136+
duration: 233.299417ms
137+
- id: 4
138+
request:
139+
proto: HTTP/1.1
140+
proto_major: 1
141+
proto_minor: 1
142+
content_length: 0
143+
transfer_encoding: []
144+
trailer: {}
145+
host: api.datadoghq.com
146+
remote_addr: ""
147+
request_uri: ""
148+
body: ""
149+
form: {}
150+
headers:
151+
Accept:
152+
- application/json
153+
url: https://api.datadoghq.com/api/v2/agentless_scanning/accounts/gcp
154+
method: GET
155+
response:
156+
proto: HTTP/1.1
157+
proto_major: 1
158+
proto_minor: 1
159+
transfer_encoding: []
160+
trailer: {}
161+
content_length: 11
162+
uncompressed: false
163+
body: '{"data":[]}'
164+
headers:
165+
Content-Type:
166+
- application/vnd.api+json
167+
status: 200 OK
168+
code: 200
169+
duration: 223.779208ms
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2025-11-06T16:33:46.401715+01:00

0 commit comments

Comments
 (0)