Skip to content

Commit a511886

Browse files
jaymclaude
andauthored
Implement drift detection for mondoo_policy_assignment resource (#403)
The Read() function was empty, only persisting existing Terraform state without querying the API. This meant changes made outside Terraform (e.g., via UI) were never detected. Now uses the activePolicies GraphQL API to fetch actual policy states, filtered by assignedScope to only consider directly-assigned policies. Compares each policy's actual action (ACTIVE/IGNORE/missing) against the configured state and updates Terraform state to reflect reality when drift is detected. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2da7160 commit a511886

2 files changed

Lines changed: 97 additions & 1 deletion

File tree

internal/provider/gql.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,22 @@ type Policy struct {
353353
Docs mondoov1.String
354354
}
355355

356+
type ActivePolicy struct {
357+
Mrn mondoov1.String
358+
Name mondoov1.String
359+
Action mondoov1.String
360+
AssignedScope mondoov1.String
361+
}
362+
363+
type ActivePolicyEdge struct {
364+
Node ActivePolicy
365+
}
366+
367+
type ActivePoliciesConnection struct {
368+
TotalCount int
369+
Edges []ActivePolicyEdge
370+
}
371+
356372
type PolicyNode struct {
357373
Policy Policy
358374
}
@@ -478,6 +494,31 @@ func (c *ExtendedGqlClient) GetPolicy(ctx context.Context, policyMrn string, spa
478494
return &q.Policy, nil
479495
}
480496

497+
func (c *ExtendedGqlClient) GetActivePolicies(ctx context.Context, scopeMrn string) ([]ActivePolicy, error) {
498+
var q struct {
499+
ActivePolicies ActivePoliciesConnection `graphql:"activePolicies(input: $input)"`
500+
}
501+
502+
input := mondoov1.ActivePoliciesInput{
503+
ScopeMrn: mondoov1.String(scopeMrn),
504+
}
505+
506+
variables := map[string]interface{}{
507+
"input": input,
508+
}
509+
510+
err := c.Query(ctx, &q, variables)
511+
if err != nil {
512+
return nil, err
513+
}
514+
515+
policies := make([]ActivePolicy, 0, len(q.ActivePolicies.Edges))
516+
for _, edge := range q.ActivePolicies.Edges {
517+
policies = append(policies, edge.Node)
518+
}
519+
return policies, nil
520+
}
521+
481522
func (c *ExtendedGqlClient) AssignPolicy(ctx context.Context, spaceMrn string, action mondoov1.PolicyAction, policyMrns []string) error {
482523
var list *[]mondoov1.String
483524

internal/provider/policy_assignment_resource.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,62 @@ func (r *policyAssignmentResource) Read(ctx context.Context, req resource.ReadRe
154154
return
155155
}
156156

157-
// Read API call logic
157+
// Compute and validate the space
158+
space, err := r.client.ComputeSpace(data.SpaceID)
159+
if err != nil {
160+
resp.Diagnostics.AddError("Invalid Configuration", err.Error())
161+
return
162+
}
163+
ctx = tflog.SetField(ctx, "space_mrn", space.MRN())
164+
165+
// Fetch active policies from API
166+
activePolicies, err := r.client.GetActivePolicies(ctx, space.MRN())
167+
if err != nil {
168+
resp.Diagnostics.AddError("Failed to fetch active policies", err.Error())
169+
return
170+
}
171+
172+
// Build lookup map: policyMrn -> action, filtered by assignedScope
173+
policyActions := make(map[string]string)
174+
for _, p := range activePolicies {
175+
if string(p.AssignedScope) == space.MRN() {
176+
policyActions[string(p.Mrn)] = string(p.Action)
177+
}
178+
}
179+
180+
// Check the actual state of each configured policy
181+
policyMrns := []string{}
182+
data.PolicyMrns.ElementsAs(ctx, &policyMrns, false)
183+
184+
configuredState := data.State.ValueString()
185+
allMatch := true
186+
for _, mrn := range policyMrns {
187+
action, found := policyActions[mrn]
188+
var actualState string
189+
if !found {
190+
actualState = "disabled"
191+
} else {
192+
switch action {
193+
case "ACTIVE":
194+
actualState = "enabled"
195+
case "IGNORE":
196+
actualState = "preview"
197+
default:
198+
actualState = "disabled"
199+
}
200+
}
201+
if actualState != configuredState {
202+
allMatch = false
203+
// Report the actual state of this policy so Terraform sees the drift
204+
data.State = types.StringValue(actualState)
205+
break
206+
}
207+
}
208+
209+
if allMatch {
210+
// All policies match the configured state, no drift
211+
data.State = types.StringValue(configuredState)
212+
}
158213

159214
// Save updated data into Terraform state
160215
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)

0 commit comments

Comments
 (0)