Go-хуки Application

Уровень: advanced ~60 мин Практика: Umami (MIT)

Что нужно знать перед уроком

  • M5.07 — Множественные инстансы одного пакета — нужно понимать модель namespace-изоляции Application перед разбором того, как хуки эту изоляцию соблюдают
  • M2.02 — Как устроен Deckhouse изнутри — общее представление о типах хуков модулей для контраста с хуками Application

Что нужно знать перед уроком

M2 урок 2 — типы хуков Module (OnStartup/BeforeHelm/… — только обзорно, не для написания). M5 урок 5 — хуки Application жёстко ограничены собственным namespace. Здесь — как реально пишется код хука Application.

Теория

ApplicationHookInput — базовый хук

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
    "context"

    "github.com/deckhouse/module-sdk/pkg"
    applicationhook "github.com/deckhouse/module-sdk/pkg/app-hook"
)

func main() {
    applicationhook.Run(onSync)
}

func onSync(ctx context.Context, input applicationhook.ApplicationHookInput) error {
    instance := input.Instance()
    // instance.Name — имя ресурса Application
    // instance.Namespace — namespace, где установлен Application

    patcher := input.ObjectPatcher()
    // patcher работает ТОЛЬКО в пределах instance.Namespace

    return nil
}

input.Instance() даёт доступ к метаданным конкретного установленного инстанса — то же смысловое значение, что и .Application.Instance.Name в шаблонах (M5 урок 4), но со стороны программируемой Go-логики, а не шаблонизации. input.ObjectPatcher() — специальный клиент для создания/изменения ресурсов, архитектурно ограниченный namespace конкретного инстанса (M5 урок 5) — попытка адресовать ресурс в другом namespace будет отклонена во время выполнения, а не просто «не сработает по недосмотру».

Чем отличается от HookInput модулей

Хуки Module (упомянутые обзорно в M2 уроке 2) используют более широкий HookInput, не ограниченный namespace — логично, т.к. Module по определению работает на уровне всего кластера. ApplicationHookInput — специализированный, более узкий интерфейс именно потому, что Application по определению должен оставаться в рамках namespace (M5 урок 5) — сама сигнатура типа в SDK физически не даёт возможности выйти за эту границу, а не просто документирует правило, которое разработчик обязан не нарушать по доброй воле.

settingscheck.Input/Result — валидация настроек сверх JSON Schema

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func Check(_ context.Context, input settingscheck.Input) settingscheck.Result {
    replicas := input.Settings.Get("replicas").Int()
    if replicas == 0 {
        return settingscheck.Reject("replicas cannot be 0")
    }

    var warnings []string
    if replicas == 2 {
        warnings = append(warnings, "чётное число реплик может привести к split-brain в некоторых конфигурациях")
    }

    if replicas > 3 {
        return settingscheck.Reject("replicas cannot be greater than 3")
    }

    return settingscheck.Allow(warnings...)
}

Этот тип хука решает задачу, которую не может решить обычная JSON Schema-валидация (M5 урок 3): JSON Schema прекрасно проверяет тип/диапазон/обязательность отдельного поля (minimum/maximum для числа), но не может выразить бизнес-логику, зависящую от комбинации нескольких полей или требующую нетривиального вычисления. Пример из документации показывает именно такой случай: replicas == 0 отклоняется (Reject), replicas == 2 разрешается, но с предупреждением (Allow с warning про потенциальный split-brain — специфика распределённых систем, которую JSON Schema принципиально не может выразить), replicas > 3 снова отклоняется.

Хук вызывается до того, как DKP применит изменения (M2 урок 3, статусная модель ConfigurationApplied) — если хук возвращает Reject, condition ConfigurationApplied перейдёт в False с reason: ConfigurationFailed (M5 урок 10), не давая невалидной конфигурации дойти до реального Nelm-рендера.

Когда settingscheck нужен, а когда достаточно JSON Schema

СитуацияДостаточно JSON Schema?Нужен settingscheck
Поле должно быть числом от 1 до 10да (minimum/maximum)нет
Поле обязательно, если задано другое полечастично (if/then в JSON Schema сложны для читаемости)часто удобнее и понятнее через хук
Комбинация нескольких полей должна соответствовать бизнес-правилу (нечётное число реплик для кворума, суммарный лимит ресурсов не должен превышать квоту)нетда
Предупреждение (не блокирующая ошибка) о потенциально проблемной, но допустимой конфигурациинет (JSON Schema либо валидна, либо нет — нет концепции «предупреждения»)да, через Allow(warnings...)

Частые ошибки и подводные камни

  • Пытаться выполнить cluster-wide операцию через ObjectPatcher хука Application. Это архитектурно заблокировано в самом SDK — попытка адресовать ресурс вне namespace инстанса вернёт ошибку выполнения, а не тихо проигнорируется.
  • Дублировать в settingscheck проверки, которые JSON Schema уже покрывает. Если проверку можно выразить декларативно через openapi/config-values.yaml (M5 урок 3) — используйте её, оставляя Go-хуки для действительно нетривиальной логики, требующей вычислений или комбинации полей.
  • Забыть, что Reject блокирует именно ConfigurationApplied, а не всю установку целиком. Пользователь увидит понятную причину отказа в статусной модели (M5 урок 10), но сам ресурс Application останется существовать в кластере, просто не сможет перейти в готовое состояние, пока настройки не будут исправлены.

Практика в кластере

Практики в кластере в этом уроке нет — компиляция и логика хука проверяются локально, поведение хука в связке со статусной моделью — в конце модуля (M5 урок 10) и в M6 при полном сценарии зависимостей.

Практика разработки

🧩 Практика урока: Umami · лицензия MIT
  1. Добавьте хук валидации настроек для Umami в hooks/batch/settings-check/main.go, ограничивающий replicas разумным диапазоном и предупреждающий о потенциальных проблемах:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
    "context"

    "github.com/deckhouse/module-sdk/pkg/settingscheck"
)

func main() {
    settingscheck.Run(Check)
}

func Check(_ context.Context, input settingscheck.Input) settingscheck.Result {
    replicas := input.Settings.Get("replicas").Int()
    hostname := input.Settings.Get("hostname").String()

    if replicas == 0 {
        return settingscheck.Reject("replicas не может быть равен 0")
    }
    if hostname == "" {
        return settingscheck.Reject("hostname обязателен для доступа к веб-интерфейсу Umami")
    }

    var warnings []string
    if replicas > 3 {
        warnings = append(warnings, "более 3 реплик Umami обычно избыточно для типичной нагрузки веб-аналитики")
    }

    return settingscheck.Allow(warnings...)
}
  1. Соберите хук локально, чтобы убедиться в отсутствии ошибок компиляции:
1
2
cd hooks/batch/settings-check
go build -o /tmp/settings-check .
  1. Добавьте базовый хук синхронизации (пока без реальной логики, просто для проверки доступа к метаданным инстанса):
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
    "context"
    "log"

    applicationhook "github.com/deckhouse/module-sdk/pkg/app-hook"
)

func main() {
    applicationhook.Run(onSync)
}

func onSync(ctx context.Context, input applicationhook.ApplicationHookInput) error {
    instance := input.Instance()
    log.Printf("синхронизация инстанса %s в namespace %s", instance.Name, instance.Namespace)
    return nil
}

Шпаргалка команд урока

1
go build -o <output> ./hooks/batch/<hook-name>

Вопросы для самопроверки

Чем ApplicationHookInput отличается от обычного HookInput модулей?

Application по определению namespace-scoped, поэтому и его SDK-интерфейс физически ограничивает область действия.

Источник: Deckhouse — Хуки Application

Когда нужен settingscheck вместо обычной JSON Schema валидации?

JSON Schema хорошо покрывает проверку отдельных полей, но не сложную межполевую логику и не концепцию предупреждений.

Источник: Deckhouse — Хуки Application

Что возвращает settingscheck.Allow(warnings...)?

Allow с warnings позволяет продолжить, но сигнализирует пользователю о потенциально проблемной, но допустимой конфигурации.

Источник: Deckhouse — Хуки Application

Что произойдёт, если settingscheck вернёт Reject?

Reject блокирует именно применение конфигурации, отражаясь в статусной модели, а не удаляет ресурс целиком.

Источник: Deckhouse — Troubleshooting

Может ли ObjectPatcher хука Application создать ресурс в другом namespace, чем сам инстанс?

Это ключевая гарантия изоляции мультитенантности, реализованная непосредственно в самом ObjectPatcher.

Источник: Deckhouse — Хуки Application

Рекомендуемая литература

Официальная документация

Статьи и блоги

Книги

  • Marko Lukša. Kubernetes in Action. 2nd ed., 2022. ISBN 978-1617297618.

Связанные материалы