Skip to content

Commit 4a92ce8

Browse files
committed
Fix: handle machinepool user-data changes (#513)
This fixes the issue with some machine pool reconciles not happening due to too restrictive checks on changes. In issue #510 we striped out `user_data` for comparison, which was too strict. This caused issue #512. Now we hash `user_data` to handle `user_data` change case. We also still allow bootstrap tokens to cause reconciliation to allow instances to join the cluster. This adds better logging around the reason the new instance config is created.
1 parent 8457071 commit 4a92ce8

File tree

6 files changed

+1189
-110
lines changed

6 files changed

+1189
-110
lines changed

cloud/hash/instanceconfighash.go

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ package hash
1818

1919
import (
2020
"crypto/sha256"
21+
"encoding/base64"
2122
"encoding/hex"
2223
"encoding/json"
2324
"fmt"
2425
"reflect"
2526
"sort"
27+
"strings"
2628

2729
"github.com/oracle/oci-go-sdk/v65/core"
2830
"github.com/pkg/errors"
@@ -139,10 +141,6 @@ func computeProjectedHash(projected *comparableLaunchDetails) (string, error) {
139141
return hex.EncodeToString(sum[:]), nil
140142
}
141143

142-
func normalizeLaunchDetails(in *core.InstanceConfigurationLaunchInstanceDetails) *comparableLaunchDetails {
143-
return projectLaunchDetails(in, in)
144-
}
145-
146144
func projectLaunchDetails(in, mask *core.InstanceConfigurationLaunchInstanceDetails) *comparableLaunchDetails {
147145
if in == nil {
148146
return nil
@@ -170,14 +168,16 @@ func projectLaunchDetails(in, mask *core.InstanceConfigurationLaunchInstanceDeta
170168
}
171169
}
172170

173-
// normalizeMetadata filters instance metadata to exclude fields like user_data
171+
// normalizeMetadata returns a copy of the metadata map with user_data excluded.
172+
// Bootstrap data (user_data) changes are tracked separately via the
173+
// BootstrapDataHashAnnotation to avoid conflating infrastructure config
174+
// changes with bootstrap secret changes.
174175
func normalizeMetadata(md map[string]string) map[string]string {
175176
if md == nil {
176177
return nil
177178
}
178179
output := make(map[string]string, len(md))
179180
for k, v := range md {
180-
// exclude user_data
181181
if k == "user_data" {
182182
continue
183183
}
@@ -189,16 +189,85 @@ func normalizeMetadata(md map[string]string) map[string]string {
189189
return output
190190
}
191191

192-
// hashChanged returns true if the two hashes are different, indicating a configuration change
193-
func hashChanged(hash1, hash2 string) bool {
194-
return hash1 != hash2
192+
// ComputeUserDataHash computes a SHA-256 hash of the raw user_data value from
193+
// instance metadata.
194+
func ComputeUserDataHash(metadata map[string]string) string {
195+
ud, ok := metadata["user_data"]
196+
if !ok {
197+
return ""
198+
}
199+
sum := sha256.Sum256([]byte(ud))
200+
return hex.EncodeToString(sum[:])
201+
}
202+
203+
// ComputeUserDataHashIgnoringKubeadmToken computes a SHA-256 hash of user_data
204+
// with kubeadm discovery token lines normalized away. This lets callers detect
205+
// token-only rotation separately from substantive bootstrap changes.
206+
func ComputeUserDataHashIgnoringKubeadmToken(metadata map[string]string) string {
207+
ud, ok := metadata["user_data"]
208+
if !ok {
209+
return ""
210+
}
211+
sum := sha256.Sum256([]byte(normalizeUserDataForHash(ud)))
212+
return hex.EncodeToString(sum[:])
213+
}
214+
215+
func normalizeUserDataForHash(userData string) string {
216+
decoded, err := base64.StdEncoding.DecodeString(userData)
217+
if err != nil {
218+
return userData
219+
}
220+
221+
normalized := string(decoded)
222+
if !strings.Contains(normalized, "kind: JoinConfiguration") || !strings.Contains(normalized, "bootstrapToken:") {
223+
return normalized
224+
}
225+
return normalizeKubeadmDiscoveryBootstrapToken(normalized)
226+
}
227+
228+
// normalizeKubeadmDiscoveryBootstrapToken rewrites only
229+
// JoinConfiguration.discovery.bootstrapToken.token values to a fixed sentinel.
230+
// Why: CABPK rotates this token frequently; we want a token-insensitive hash for
231+
// classification/observability. Reconcile still uses the raw user_data hash for
232+
// drift decisions, so non-token bootstrap changes are not ignored.
233+
func normalizeKubeadmDiscoveryBootstrapToken(cloudInitData string) string {
234+
lines := strings.Split(cloudInitData, "\n")
235+
discoveryIndent := -1
236+
bootstrapTokenIndent := -1
237+
238+
for i, line := range lines {
239+
trimmed := strings.TrimSpace(line)
240+
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
241+
continue
242+
}
243+
244+
indent := leadingIndentWidth(line)
245+
246+
if bootstrapTokenIndent >= 0 && indent <= bootstrapTokenIndent {
247+
bootstrapTokenIndent = -1
248+
}
249+
if discoveryIndent >= 0 && indent <= discoveryIndent {
250+
discoveryIndent = -1
251+
}
252+
253+
if trimmed == "discovery:" {
254+
discoveryIndent = indent
255+
continue
256+
}
257+
if discoveryIndent >= 0 && indent > discoveryIndent && trimmed == "bootstrapToken:" {
258+
bootstrapTokenIndent = indent
259+
continue
260+
}
261+
if bootstrapTokenIndent >= 0 && indent > bootstrapTokenIndent && strings.HasPrefix(trimmed, "token:") {
262+
lines[i] = line[:indent] + "token: <redacted>"
263+
}
264+
}
265+
266+
return strings.Join(lines, "\n")
195267
}
196268

197-
// launchDetailsEqual returns true if two launch details are equivalent after normalization
198-
func launchDetailsEqual(ld1, ld2 *core.InstanceConfigurationLaunchInstanceDetails) bool {
199-
normalized1 := normalizeLaunchDetails(ld1)
200-
normalized2 := normalizeLaunchDetails(ld2)
201-
return reflect.DeepEqual(normalized1, normalized2)
269+
func leadingIndentWidth(s string) int {
270+
return len(s) - len(strings.TrimLeft(s, " \t"))
202271
}
203272

204273
func projectCreateVnicDetails(in, mask *core.InstanceConfigurationCreateVnicDetails) *comparableCreateVnicDetails {

0 commit comments

Comments
 (0)