Skip to content

Commit 01a2165

Browse files
Fkuloffipaqsa
andauthored
[feat] ensure conversion hooks (#790)
Signed-off-by: Stepan Paksashvili <stepan.paksashvili@flant.com> Signed-off-by: Artem Kuleshov <artem.kuleshov@flant.com> Co-authored-by: Stepan Paksashvili <81509933+ipaqsa@users.noreply.github.com>
1 parent 51ff1da commit 01a2165

10 files changed

Lines changed: 276 additions & 9 deletions

File tree

pkg/addon-operator/converge/converge.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func IsConvergeTask(t sh_task.Task) bool {
121121
hm := task.HookMetadataAccessor(t)
122122

123123
switch taskType {
124-
case task.ModuleDelete, task.ConvergeModules, task.ModuleEnsureCRDs:
124+
case task.ModuleDelete, task.ConvergeModules, task.ModuleEnsureCRDs, task.ModuleEnsureHooks:
125125
return true
126126
case task.ModuleRun, task.ParallelModuleRun:
127127
return hm.IsReloadAll

pkg/addon-operator/operator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -924,7 +924,7 @@ func formatTaskDetails(tsk sh_task.Task, hm task.HookMetadata, phase string) str
924924
case task.ParallelModuleRun:
925925
return fmt.Sprintf(" for modules '%s'", hm.ModuleName)
926926

927-
case task.ModulePurge, task.ModuleDelete, task.ModuleEnsureCRDs:
927+
case task.ModulePurge, task.ModuleDelete, task.ModuleEnsureCRDs, task.ModuleEnsureHooks:
928928
return fmt.Sprintf(" for module '%s'", hm.ModuleName)
929929

930930
case task.GlobalHookEnableKubernetesBindings,

pkg/module_manager/models/modules/basic.go

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ type BasicModule struct {
6161
crdsExist bool
6262
crdFilesPaths []string
6363

64+
conversionWebhooksExist bool
65+
6466
valuesStorage *ValuesStorage
6567

6668
hooks *HooksStorage
@@ -88,12 +90,13 @@ func NewBasicModule(name, path string, order uint32, staticValues utils.Values,
8890

8991
crdsFromPath := getCRDsFromPath(path, app.CRDsFilters)
9092
bmodule := &BasicModule{
91-
Name: name,
92-
Order: order,
93-
Path: path,
94-
crdsExist: len(crdsFromPath) > 0,
95-
crdFilesPaths: crdsFromPath,
96-
valuesStorage: valuesStorage,
93+
Name: name,
94+
Order: order,
95+
Path: path,
96+
crdsExist: len(crdsFromPath) > 0,
97+
crdFilesPaths: crdsFromPath,
98+
conversionWebhooksExist: templatesHaveConversionWebhook(path),
99+
valuesStorage: valuesStorage,
97100
state: &moduleState{
98101
Phase: Startup,
99102
hookErrors: make(map[string]error),
@@ -155,6 +158,43 @@ func getCRDsFromPath(path string, crdsFilters string) []string {
155158
return crdFilesPaths
156159
}
157160

161+
// templatesHaveConversionWebhook reports whether any template file under
162+
// path/templates mentions a ConversionWebhook resource. It is a cheap static
163+
// heuristic (the kind is virtually always a literal in manifests) used to avoid
164+
// rendering charts that have no conversion webhooks. A false positive only
165+
// results in a render that finds nothing, so it is safe.
166+
func templatesHaveConversionWebhook(path string) bool {
167+
var found bool
168+
169+
err := filepath.Walk(
170+
filepath.Join(path, "templates"),
171+
func(path string, info os.FileInfo, err error) error {
172+
if err != nil {
173+
return err
174+
}
175+
176+
if found || info.IsDir() {
177+
return nil
178+
}
179+
180+
data, err := os.ReadFile(path)
181+
if err != nil {
182+
return err
183+
}
184+
185+
if strings.Contains(string(data), ConversionWebhookKind) {
186+
found = true
187+
}
188+
189+
return nil
190+
})
191+
if err != nil {
192+
return false
193+
}
194+
195+
return found
196+
}
197+
158198
func matchPrefix(path string, crdsFilters string) bool {
159199
filters := strings.Split(crdsFilters, ",")
160200
for _, filter := range filters {
@@ -1467,6 +1507,12 @@ func (bm *BasicModule) CRDExist() bool {
14671507
return bm.crdsExist
14681508
}
14691509

1510+
// ConversionWebhookExist reports whether the module's templates contain at least
1511+
// one ConversionWebhook resource, computed once when the module is loaded.
1512+
func (bm *BasicModule) ConversionWebhookExist() bool {
1513+
return bm.conversionWebhooksExist
1514+
}
1515+
14701516
type ModuleRunPhase string
14711517

14721518
const (

pkg/module_manager/models/modules/helm.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,3 +455,34 @@ func (hm *HelmModule) Render(namespace string, debug bool, state MaintenanceStat
455455

456456
return hm.dependencies.HelmClientFactory.NewClient(hm.logger.Named("helm-client"), helmClientOptions...).Render(hm.name, hm.path, []string{valuesPath}, nil, releaseLabels, namespace, debug)
457457
}
458+
459+
// ConversionWebhookKind is the kind of the rendered resource that carries a CRD
460+
// conversion webhook configuration. Such resources must be applied to the cluster
461+
// before the main helm release, so the release can adopt them afterwards.
462+
const ConversionWebhookKind = "ConversionWebhook"
463+
464+
// RenderConversionWebhooks renders the module templates and returns only the
465+
// manifests of kind ConversionWebhook. They are extracted from the chart so the
466+
// caller can apply them ahead of the main helm install, letting the subsequent
467+
// release adopt the already-present resources.
468+
func (hm *HelmModule) RenderConversionWebhooks(namespace string, state MaintenanceState) ([]manifest.Manifest, error) {
469+
rendered, err := hm.Render(namespace, false, state)
470+
if err != nil {
471+
return nil, fmt.Errorf("render templates: %w", err)
472+
}
473+
474+
all, err := manifest.ListFromYamlDocs(rendered)
475+
if err != nil {
476+
return nil, fmt.Errorf("split rendered manifests: %w", err)
477+
}
478+
479+
webhooks := make([]manifest.Manifest, 0)
480+
481+
for _, m := range all {
482+
if m.Kind() == ConversionWebhookKind {
483+
webhooks = append(webhooks, m)
484+
}
485+
}
486+
487+
return webhooks, nil
488+
}

pkg/module_manager/module_manager.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/deckhouse/deckhouse/pkg/log"
1515
metricsstorage "github.com/deckhouse/deckhouse/pkg/metrics-storage"
16+
sdkpkg "github.com/deckhouse/module-sdk/pkg"
1617
"github.com/hashicorp/go-multierror"
1718
"go.opentelemetry.io/otel"
1819

@@ -867,6 +868,70 @@ func (mm *ModuleManager) RunModule(ctx context.Context, moduleName string, logLa
867868
return oldValuesChecksum != newValuesChecksum, nil
868869
}
869870

871+
// EnsureConversionWebhooks renders the module's ConversionWebhook resources and
872+
// applies them to the cluster before the main helm install. The subsequent
873+
// release adopts the already-present resources. It is a no-op for non-helm
874+
// modules and for modules without ConversionWebhook templates.
875+
func (mm *ModuleManager) EnsureConversionWebhooks(moduleName string, logLabels map[string]string) error {
876+
bm := mm.GetModule(moduleName)
877+
if bm == nil {
878+
return fmt.Errorf("module %q not found", moduleName)
879+
}
880+
881+
schemaStorage := bm.GetValuesStorage().GetSchemaStorage()
882+
deps := &modules.HelmModuleDependencies{
883+
HelmClientFactory: mm.dependencies.Helm,
884+
HelmResourceManager: mm.dependencies.HelmResourcesManager,
885+
MetricsStorage: mm.dependencies.MetricStorage,
886+
HelmValuesValidator: schemaStorage,
887+
}
888+
889+
helmModule, err := modules.NewHelmModule(bm, mm.defaultNamespace, mm.TempDir, deps, schemaStorage, modules.WithLogger(mm.logger.Named("helm-module")))
890+
if err != nil {
891+
if errors.Is(err, modules.ErrModuleIsNotHelm) {
892+
return nil
893+
}
894+
895+
return fmt.Errorf("create helm module: %w", err)
896+
}
897+
898+
webhooks, err := helmModule.RenderConversionWebhooks(mm.defaultNamespace, bm.GetMaintenanceState())
899+
if err != nil {
900+
return fmt.Errorf("render conversion webhooks: %w", err)
901+
}
902+
903+
if len(webhooks) == 0 {
904+
return nil
905+
}
906+
907+
ops := make([]sdkpkg.PatchCollectorOperation, 0, len(webhooks))
908+
for _, m := range webhooks {
909+
ops = append(ops, objectpatch.NewCreateOrUpdateOperation(map[string]any(m)))
910+
}
911+
912+
if err := mm.dependencies.KubeObjectPatcher.ExecuteOperations(ops); err != nil {
913+
return fmt.Errorf("apply conversion webhooks: %w", err)
914+
}
915+
916+
utils.EnrichLoggerWithLabels(mm.logger, logLabels).Debug("applied conversion webhooks",
917+
slog.String(pkg.LogKeyModule, moduleName),
918+
slog.Int(pkg.LogKeyCount, len(webhooks)))
919+
920+
return nil
921+
}
922+
923+
// ModuleHasConversionWebhooks reports whether the module's templates contain at
924+
// least one ConversionWebhook resource. It is a cheap static check used to skip
925+
// rendering for modules that have no conversion webhooks.
926+
func (mm *ModuleManager) ModuleHasConversionWebhooks(moduleName string) bool {
927+
bm := mm.GetModule(moduleName)
928+
if bm == nil {
929+
return false
930+
}
931+
932+
return bm.ConversionWebhookExist()
933+
}
934+
870935
func (mm *ModuleManager) RunGlobalHook(ctx context.Context, hookName string, binding BindingType, bindingContext []BindingContext, logLabels map[string]string) (string, string, error) {
871936
return mm.global.RunHookByName(ctx, hookName, binding, bindingContext, logLabels)
872937
}

pkg/task/helpers/helpers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func formatTaskDetails(tsk sh_task.Task, hm task.HookMetadata, phase string) str
7575
case task.ParallelModuleRun:
7676
return fmt.Sprintf(" for modules '%s'", hm.ModuleName)
7777

78-
case task.ModulePurge, task.ModuleDelete, task.ModuleEnsureCRDs:
78+
case task.ModulePurge, task.ModuleDelete, task.ModuleEnsureCRDs, task.ModuleEnsureHooks:
7979
return fmt.Sprintf(" for module '%s'", hm.ModuleName)
8080

8181
case task.GlobalHookEnableKubernetesBindings,

pkg/task/service/service.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
globalhookwaitkubernetessynchronization "github.com/flant/addon-operator/pkg/task/tasks/global-hook-wait-kubernetes-synchronization"
3030
moduledelete "github.com/flant/addon-operator/pkg/task/tasks/module-delete"
3131
moduleensurecrds "github.com/flant/addon-operator/pkg/task/tasks/module-ensure-crds"
32+
moduleensurehooks "github.com/flant/addon-operator/pkg/task/tasks/module-ensure-hooks"
3233
modulehookrun "github.com/flant/addon-operator/pkg/task/tasks/module-hook-run"
3334
modulepurge "github.com/flant/addon-operator/pkg/task/tasks/module-purge"
3435
modulerun "github.com/flant/addon-operator/pkg/task/tasks/module-run"
@@ -238,6 +239,7 @@ func (s *TaskHandlerService) initFactory() {
238239
task.ModuleHookRun: modulehookrun.RegisterTaskHandler(s),
239240
task.ModulePurge: modulepurge.RegisterTaskHandler(s),
240241
task.ModuleEnsureCRDs: moduleensurecrds.RegisterTaskHandler(s),
242+
task.ModuleEnsureHooks: moduleensurehooks.RegisterTaskHandler(s),
241243
task.ModuleRun: modulerun.RegisterTaskHandler(s),
242244
task.ConvergeModules: convergemodules.RegisterTaskHandler(s),
243245
task.ParallelModuleRun: parallelmodulerun.RegisterTaskHandler(s),

pkg/task/task.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const (
2323
ModulePurge task.TaskType = "ModulePurge"
2424
// ModuleEnsureCRDs runs ensureCRDs task for enabled module
2525
ModuleEnsureCRDs task.TaskType = "ModuleEnsureCRDs"
26+
// ModuleEnsureHooks applies a module's ConversionWebhook resources before its helm release.
27+
ModuleEnsureHooks task.TaskType = "ModuleEnsureHooks"
2628

2729
// DiscoverHelmReleases lists helm releases to detect unknown modules and initiate enabled modules list.
2830
DiscoverHelmReleases task.TaskType = "DiscoverHelmReleases"

pkg/task/tasks/converge-modules/task.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,11 @@ func (s *Task) CreateConvergeModulesTasks(state *module_manager.ModulesState, lo
445445
doModuleStartup = true
446446
}
447447

448+
// add EnsureHooks after the optional EnsureCRDs, before ModuleRun
449+
if s.moduleManager.ModuleHasConversionWebhooks(moduleName) {
450+
resultingTasks = append(resultingTasks, s.newEnsureHooksTask(moduleName, newLogLabels, queuedAt))
451+
}
452+
448453
parallelRunMetadata.SetModuleMetadata(moduleName, task.ParallelRunModuleMetadata{
449454
DoModuleStartup: doModuleStartup,
450455
})
@@ -490,6 +495,11 @@ func (s *Task) CreateConvergeModulesTasks(state *module_manager.ModulesState, lo
490495
doModuleStartup = true
491496
}
492497

498+
// add EnsureHooks after the optional EnsureCRDs, before ModuleRun
499+
if s.moduleManager.ModuleHasConversionWebhooks(modules[0]) {
500+
resultingTasks = append(resultingTasks, s.newEnsureHooksTask(modules[0], newLogLabels, queuedAt))
501+
}
502+
493503
newTask := sh_task.NewTask(task.ModuleRun).
494504
WithLogLabels(newLogLabels).
495505
WithQueueName("main").
@@ -541,6 +551,11 @@ func (s *Task) CreateConvergeModulesTasks(state *module_manager.ModulesState, lo
541551
doModuleStartup = true
542552
}
543553

554+
// add EnsureHooks after the optional EnsureCRDs, before ModuleRun
555+
if s.moduleManager.ModuleHasConversionWebhooks(module) {
556+
resultingTasks = append(resultingTasks, s.newEnsureHooksTask(module, newLogLabels, queuedAt))
557+
}
558+
544559
schedulerRequests[idx] = &functional.Request{
545560
Name: module,
546561
Dependencies: deps[module],
@@ -570,6 +585,19 @@ func (s *Task) CreateConvergeModulesTasks(state *module_manager.ModulesState, lo
570585
return resultingTasks
571586
}
572587

588+
// newEnsureHooksTask builds a ModuleEnsureHooks task that applies the module's
589+
// ConversionWebhook resources before its helm release.
590+
func (s *Task) newEnsureHooksTask(moduleName string, logLabels map[string]string, queuedAt time.Time) sh_task.Task {
591+
return sh_task.NewTask(task.ModuleEnsureHooks).
592+
WithLogLabels(logLabels).
593+
WithQueueName("main").
594+
WithMetadata(task.HookMetadata{
595+
EventDescription: "EnsureHooks",
596+
ModuleName: moduleName,
597+
IsReloadAll: true,
598+
}).WithQueuedAt(queuedAt)
599+
}
600+
573601
func (s *Task) IsStartupConvergeDone() bool {
574602
return s.convergeState.GetFirstRunPhase() == converge.FirstDone
575603
}

0 commit comments

Comments
 (0)