@@ -82,28 +82,75 @@ func (r *okmsSecretResource) Create(ctx context.Context, req resource.CreateRequ
8282}
8383
8484func (r * okmsSecretResource ) Read (ctx context.Context , req resource.ReadRequest , resp * resource.ReadResponse ) {
85- var data , responseData OkmsSecretModel
85+ var secretFromState , currentSecret OkmsSecretModel
8686
8787 // Read Terraform prior state data into the model
88- resp .Diagnostics .Append (req .State .Get (ctx , & data )... )
88+ resp .Diagnostics .Append (req .State .Get (ctx , & secretFromState )... )
8989 if resp .Diagnostics .HasError () {
9090 return
9191 }
9292
93- endpoint := "/v2/okms/resource/" + url .PathEscape (data .OkmsId .ValueString ()) + "/secret/" + url .PathEscape (data .Path .ValueString ()) + ""
93+ endpoint := "/v2/okms/resource/" + url .PathEscape (secretFromState .OkmsId .ValueString ()) + "/secret/" + url .PathEscape (secretFromState .Path .ValueString ())
9494
95- if err := r .config .OVHClient .Get (endpoint , & responseData ); err != nil {
95+ if err := r .config .OVHClient .Get (endpoint , & currentSecret ); err != nil {
9696 resp .Diagnostics .AddError (
9797 fmt .Sprintf ("Error calling Get %s" , endpoint ),
9898 err .Error (),
9999 )
100100 return
101101 }
102102
103- data .MergeWith (& responseData )
103+ // Update metadata and IAM from API response
104+ secretFromState .Metadata = currentSecret .Metadata
105+ secretFromState .Iam = currentSecret .Iam
106+
107+ // Fetch the current version details including data for drift detection
108+ currentVersion := currentSecret .Metadata .CurrentVersion .ValueInt64 ()
109+ versionEndpoint := "/v2/okms/resource/" + url .PathEscape (secretFromState .OkmsId .ValueString ()) + "/secret/" + url .PathEscape (secretFromState .Path .ValueString ()) + "/version/" + fmt .Sprintf ("%d" , currentVersion ) + "?includeData=true"
110+
111+ var currentSecretVersion struct {
112+ Id * int64 `json:"id"`
113+ CreatedAt * string `json:"createdAt"`
114+ State * string `json:"state"`
115+ DeactivatedAt * string `json:"deactivatedAt"`
116+ Data json.RawMessage `json:"data"`
117+ }
118+ if err := r .config .OVHClient .Get (versionEndpoint , & currentSecretVersion ); err == nil {
119+ // Update version computed fields
120+ if currentSecretVersion .Id != nil {
121+ secretFromState .Version .Id = ovhtypes .NewTfInt64Value (* currentSecretVersion .Id )
122+ }
123+ if currentSecretVersion .CreatedAt != nil {
124+ secretFromState .Version .CreatedAt = ovhtypes .NewTfStringValue (* currentSecretVersion .CreatedAt )
125+ }
126+ if currentSecretVersion .State != nil {
127+ secretFromState .Version .State = ovhtypes .NewTfStringValue (* currentSecretVersion .State )
128+ }
129+ if currentSecretVersion .DeactivatedAt != nil {
130+ secretFromState .Version .DeactivatedAt = ovhtypes .NewTfStringValue (* currentSecretVersion .DeactivatedAt )
131+ } else {
132+ secretFromState .Version .DeactivatedAt = ovhtypes .NewTfStringNull ()
133+ }
134+
135+ // Check version state and data for drift detection
136+ if currentSecretVersion .State != nil && * currentSecretVersion .State == "DELETED" {
137+ // Version is deleted - mark data as empty to force drift detection
138+ secretFromState .Version .Data = ovhtypes .NewTfStringValue ("" )
139+ } else if len (currentSecretVersion .Data ) > 0 {
140+ // Version is active - compare data for drift
141+ actualData := string (currentSecretVersion .Data )
142+ configData := secretFromState .Version .Data .ValueString ()
143+
144+ if ! semanticJSONEqual (configData , actualData ) {
145+ // Data differs - update state with actual value so Terraform shows drift
146+ secretFromState .Version .Data = ovhtypes .NewTfStringValue (actualData )
147+ }
148+ }
149+ }
150+ // Silently ignore errors - drift detection is best-effort
104151
105152 // Save updated data into Terraform state
106- resp .Diagnostics .Append (resp .State .Set (ctx , & data )... )
153+ resp .Diagnostics .Append (resp .State .Set (ctx , & secretFromState )... )
107154}
108155
109156func (r * okmsSecretResource ) Update (ctx context.Context , req resource.UpdateRequest , resp * resource.UpdateResponse ) {
@@ -244,6 +291,34 @@ func buildMetadataPayload(meta *MetadataValue) map[string]any {
244291 return mp
245292}
246293
294+ // semanticJSONEqual compares two JSON strings for semantic equality,
295+ // ignoring formatting differences like whitespace and key ordering.
296+ func semanticJSONEqual (a , b string ) bool {
297+ var objA , objB interface {}
298+
299+ // Try to parse both as JSON
300+ if err := json .Unmarshal ([]byte (a ), & objA ); err != nil {
301+ // Not valid JSON, fall back to string comparison
302+ return a == b
303+ }
304+ if err := json .Unmarshal ([]byte (b ), & objB ); err != nil {
305+ // Not valid JSON, fall back to string comparison
306+ return a == b
307+ }
308+
309+ // Re-marshal both in canonical form and compare
310+ normalizedA , err := json .Marshal (objA )
311+ if err != nil {
312+ return a == b
313+ }
314+ normalizedB , err := json .Marshal (objB )
315+ if err != nil {
316+ return a == b
317+ }
318+
319+ return string (normalizedA ) == string (normalizedB )
320+ }
321+
247322// populateVersionComputedFields fills secret version attributes
248323func populateVersionComputedFields (r * okmsSecretResource , model * OkmsSecretModel , okmsId , path string ) {
249324 // If currentVersion unknown or zero, nothing to enrich
0 commit comments