Снова про операторы.
У ресурса
Secret в Kubernetes есть два поля для данных:-
Data map[string][]byte - бинарные данные, в YAML выглядят как base64-
StringData map[string]string - строковые данные, plain text в YAMLКак это работает на самом деле:
StringData - write-only поле для удобства. Когда ты создаёшь или обновляешь секрет, API-сервер мержит
StringData в Data (ключи из StringData перезаписывают Data при совпадении). В etcd хранилке и в ответе API сохраняется только
Data. При чтении секрета из кластера поле
StringData всегда пустое - его там просто нет.Поэтому StringData - для людей и YAML-манифестов, а Data - для кода операторов.
А теперь типичнейшая ошибка, на которую я снова попался. 😭
Код оператора с ошибкой:
func BuildCredentialsSecret(instance *v1alpha1.MyApp) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: instance.Name + "-credentials",
Namespace: instance.Namespace,
},
Type: corev1.SecretTypeOpaque,
StringData: map[string]string{
"API_HOST": instance.Spec.Config.API.Host,
"API_TOKEN": instance.Spec.Config.API.Token,
},
}
}В реконсайлере проверка "изменился ли секрет?":
func secretDataEqual(a, b map[string][]byte) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if !bytes.Equal(v, b[k]) {
return false
}
}
return true
}
// в reconcile:
if !secretDataEqual(found.Data, secret.Data) {
// обновляем секрет
found.Data = secret.Data // ВОТ ТУТ ПРОБЛЕМА
return r.Update(ctx, found)
}Видите проблему?
При создании desired-секрета мы используем
StringData. Но в Go-структуре этого объекта поле
Data равно nil, потому что мы его не заполняли!!!Что происходит:
- оператор создаёт секрет с
StringData > API сервер конвертирует в Data, в кластере всё ок- при следующем reconcile оператор читает секрет - там
Data заполнен- оператор строит desired-секрет - там
Data = nil, StringData заполнен- сравнение видит разницу:
len(found.Data) != 0, а len(secret.Data) == 0
(потому что nil)
- оператор решает обновить секрет и делает
found.Data = secret.Data
записывая nil в поле данных!
- теперь в кластере секрет с
data: {}(0 ключей)
- в следующем reconcile сравнение
secretDataEqual(found.Data, nil)
оба пустые? Да!
(len(nil) == 0, len(map{}) == 0)- оператор думает: "секрет актуален", и никогда не восстанавливает данные 🤡
Результат: секрет живёт,
ownerReference на месте, лейблы есть, а данных - ноль. Поды падают с:
MountVolume.SetUp failed for volume "credentials":
references non-existent secret key: API_TOKEN
Фикс очевиден: использовать
Data в коде оператора:func BuildCredentialsSecret(instance *v1alpha1.MyApp) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: instance.Name + "-credentials",
Namespace: instance.Namespace,
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"API_HOST": []byte(instance.Spec.Config.API.Host),
"API_TOKEN": []byte(instance.Spec.Config.API.Token),
},
}
}Теперь
secret.Data заполнен, сравнение работает корректно, и ты случайно не затираешь данные nil-ом.🎉🎉🎉
Мораль:
-
stringData - для YAML и kubectl, где удобно писать plain text-
data - для кода операторов на Go- в client-go структуре
StringData и Data - разные поля; API сервер не синхронизирует их при чтении- если используешь
StringData в коде, помни: secret.Data == nil в этой структуре- сравнивай и применяй то, что реально используешь
Потерял на этом баге часа два, пока не дошло. Сука.
Секрет создавался, лейблы были,
ownerReference был, а данных - ноль. Классика - симптомы в одном месте (падающие поды), причина в другом (nil вместо мапы в билдере).