Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/addon-operator/converge/converge.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func IsConvergeTask(t sh_task.Task) bool {
hm := task.HookMetadataAccessor(t)

switch taskType {
case task.ModuleDelete, task.ConvergeModules, task.ModuleEnsureCRDs:
case task.ModuleDelete, task.ConvergeModules, task.ModuleEnsureCRDs, task.ModuleEnsureHooks:
return true
case task.ModuleRun, task.ParallelModuleRun:
return hm.IsReloadAll
Expand Down
2 changes: 1 addition & 1 deletion pkg/addon-operator/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,7 @@ func formatTaskDetails(tsk sh_task.Task, hm task.HookMetadata, phase string) str
case task.ParallelModuleRun:
return fmt.Sprintf(" for modules '%s'", hm.ModuleName)

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

case task.GlobalHookEnableKubernetesBindings,
Expand Down
58 changes: 52 additions & 6 deletions pkg/module_manager/models/modules/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ type BasicModule struct {
crdsExist bool
crdFilesPaths []string

conversionWebhooksExist bool

valuesStorage *ValuesStorage

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

crdsFromPath := getCRDsFromPath(path, app.CRDsFilters)
bmodule := &BasicModule{
Name: name,
Order: order,
Path: path,
crdsExist: len(crdsFromPath) > 0,
crdFilesPaths: crdsFromPath,
valuesStorage: valuesStorage,
Name: name,
Order: order,
Path: path,
crdsExist: len(crdsFromPath) > 0,
crdFilesPaths: crdsFromPath,
conversionWebhooksExist: templatesHaveConversionWebhook(path),
valuesStorage: valuesStorage,
state: &moduleState{
Phase: Startup,
hookErrors: make(map[string]error),
Expand Down Expand Up @@ -155,6 +158,43 @@ func getCRDsFromPath(path string, crdsFilters string) []string {
return crdFilesPaths
}

// templatesHaveConversionWebhook reports whether any template file under
// path/templates mentions a ConversionWebhook resource. It is a cheap static
// heuristic (the kind is virtually always a literal in manifests) used to avoid
// rendering charts that have no conversion webhooks. A false positive only
// results in a render that finds nothing, so it is safe.
func templatesHaveConversionWebhook(path string) bool {
var found bool

err := filepath.Walk(
filepath.Join(path, "templates"),
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

if found || info.IsDir() {
return nil
}

data, err := os.ReadFile(path)
if err != nil {
return err
}

if strings.Contains(string(data), ConversionWebhookKind) {
found = true
}

return nil
})
if err != nil {
return false
}

return found
}

func matchPrefix(path string, crdsFilters string) bool {
filters := strings.Split(crdsFilters, ",")
for _, filter := range filters {
Expand Down Expand Up @@ -1467,6 +1507,12 @@ func (bm *BasicModule) CRDExist() bool {
return bm.crdsExist
}

// ConversionWebhookExist reports whether the module's templates contain at least
// one ConversionWebhook resource, computed once when the module is loaded.
func (bm *BasicModule) ConversionWebhookExist() bool {
return bm.conversionWebhooksExist
}

type ModuleRunPhase string

const (
Expand Down
31 changes: 31 additions & 0 deletions pkg/module_manager/models/modules/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,34 @@ func (hm *HelmModule) Render(namespace string, debug bool, state MaintenanceStat

return hm.dependencies.HelmClientFactory.NewClient(hm.logger.Named("helm-client"), helmClientOptions...).Render(hm.name, hm.path, []string{valuesPath}, nil, releaseLabels, namespace, debug)
}

// ConversionWebhookKind is the kind of the rendered resource that carries a CRD
// conversion webhook configuration. Such resources must be applied to the cluster
// before the main helm release, so the release can adopt them afterwards.
const ConversionWebhookKind = "ConversionWebhook"

// RenderConversionWebhooks renders the module templates and returns only the
// manifests of kind ConversionWebhook. They are extracted from the chart so the
// caller can apply them ahead of the main helm install, letting the subsequent
// release adopt the already-present resources.
func (hm *HelmModule) RenderConversionWebhooks(namespace string, state MaintenanceState) ([]manifest.Manifest, error) {
rendered, err := hm.Render(namespace, false, state)
if err != nil {
return nil, fmt.Errorf("render templates: %w", err)
}

all, err := manifest.ListFromYamlDocs(rendered)
if err != nil {
return nil, fmt.Errorf("split rendered manifests: %w", err)
}

webhooks := make([]manifest.Manifest, 0)

for _, m := range all {
if m.Kind() == ConversionWebhookKind {
webhooks = append(webhooks, m)
}
}

return webhooks, nil
}
65 changes: 65 additions & 0 deletions pkg/module_manager/module_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/deckhouse/deckhouse/pkg/log"
metricsstorage "github.com/deckhouse/deckhouse/pkg/metrics-storage"
sdkpkg "github.com/deckhouse/module-sdk/pkg"
"github.com/hashicorp/go-multierror"
"go.opentelemetry.io/otel"

Expand Down Expand Up @@ -853,6 +854,70 @@ func (mm *ModuleManager) RunModule(ctx context.Context, moduleName string, logLa
return oldValuesChecksum != newValuesChecksum, nil
}

// EnsureConversionWebhooks renders the module's ConversionWebhook resources and
// applies them to the cluster before the main helm install. The subsequent
// release adopts the already-present resources. It is a no-op for non-helm
// modules and for modules without ConversionWebhook templates.
func (mm *ModuleManager) EnsureConversionWebhooks(moduleName string, logLabels map[string]string) error {
bm := mm.GetModule(moduleName)
if bm == nil {
return fmt.Errorf("module %q not found", moduleName)
}

schemaStorage := bm.GetValuesStorage().GetSchemaStorage()
deps := &modules.HelmModuleDependencies{
HelmClientFactory: mm.dependencies.Helm,
HelmResourceManager: mm.dependencies.HelmResourcesManager,
MetricsStorage: mm.dependencies.MetricStorage,
HelmValuesValidator: schemaStorage,
}

helmModule, err := modules.NewHelmModule(bm, mm.defaultNamespace, mm.TempDir, deps, schemaStorage, modules.WithLogger(mm.logger.Named("helm-module")))
if err != nil {
if errors.Is(err, modules.ErrModuleIsNotHelm) {
return nil
}

return fmt.Errorf("create helm module: %w", err)
}

webhooks, err := helmModule.RenderConversionWebhooks(mm.defaultNamespace, bm.GetMaintenanceState())
if err != nil {
return fmt.Errorf("render conversion webhooks: %w", err)
}

if len(webhooks) == 0 {
return nil
}

ops := make([]sdkpkg.PatchCollectorOperation, 0, len(webhooks))
for _, m := range webhooks {
ops = append(ops, objectpatch.NewCreateOrUpdateOperation(map[string]any(m)))
}

if err := mm.dependencies.KubeObjectPatcher.ExecuteOperations(ops); err != nil {
return fmt.Errorf("apply conversion webhooks: %w", err)
}

utils.EnrichLoggerWithLabels(mm.logger, logLabels).Debug("applied conversion webhooks",
slog.String(pkg.LogKeyModule, moduleName),
slog.Int(pkg.LogKeyCount, len(webhooks)))

return nil
}

// ModuleHasConversionWebhooks reports whether the module's templates contain at
// least one ConversionWebhook resource. It is a cheap static check used to skip
// rendering for modules that have no conversion webhooks.
func (mm *ModuleManager) ModuleHasConversionWebhooks(moduleName string) bool {
bm := mm.GetModule(moduleName)
if bm == nil {
return false
}

return bm.ConversionWebhookExist()
}

func (mm *ModuleManager) RunGlobalHook(ctx context.Context, hookName string, binding BindingType, bindingContext []BindingContext, logLabels map[string]string) (string, string, error) {
return mm.global.RunHookByName(ctx, hookName, binding, bindingContext, logLabels)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/task/helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func formatTaskDetails(tsk sh_task.Task, hm task.HookMetadata, phase string) str
case task.ParallelModuleRun:
return fmt.Sprintf(" for modules '%s'", hm.ModuleName)

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

case task.GlobalHookEnableKubernetesBindings,
Expand Down
2 changes: 2 additions & 0 deletions pkg/task/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
globalhookwaitkubernetessynchronization "github.com/flant/addon-operator/pkg/task/tasks/global-hook-wait-kubernetes-synchronization"
moduledelete "github.com/flant/addon-operator/pkg/task/tasks/module-delete"
moduleensurecrds "github.com/flant/addon-operator/pkg/task/tasks/module-ensure-crds"
moduleensurehooks "github.com/flant/addon-operator/pkg/task/tasks/module-ensure-hooks"
modulehookrun "github.com/flant/addon-operator/pkg/task/tasks/module-hook-run"
modulepurge "github.com/flant/addon-operator/pkg/task/tasks/module-purge"
modulerun "github.com/flant/addon-operator/pkg/task/tasks/module-run"
Expand Down Expand Up @@ -238,6 +239,7 @@ func (s *TaskHandlerService) initFactory() {
task.ModuleHookRun: modulehookrun.RegisterTaskHandler(s),
task.ModulePurge: modulepurge.RegisterTaskHandler(s),
task.ModuleEnsureCRDs: moduleensurecrds.RegisterTaskHandler(s),
task.ModuleEnsureHooks: moduleensurehooks.RegisterTaskHandler(s),
task.ModuleRun: modulerun.RegisterTaskHandler(s),
task.ConvergeModules: convergemodules.RegisterTaskHandler(s),
task.ParallelModuleRun: parallelmodulerun.RegisterTaskHandler(s),
Expand Down
2 changes: 2 additions & 0 deletions pkg/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const (
ModulePurge task.TaskType = "ModulePurge"
// ModuleEnsureCRDs runs ensureCRDs task for enabled module
ModuleEnsureCRDs task.TaskType = "ModuleEnsureCRDs"
// ModuleEnsureHooks applies a module's ConversionWebhook resources before its helm release.
ModuleEnsureHooks task.TaskType = "ModuleEnsureHooks"

// DiscoverHelmReleases lists helm releases to detect unknown modules and initiate enabled modules list.
DiscoverHelmReleases task.TaskType = "DiscoverHelmReleases"
Expand Down
28 changes: 28 additions & 0 deletions pkg/task/tasks/converge-modules/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,11 @@ func (s *Task) CreateConvergeModulesTasks(state *module_manager.ModulesState, lo
doModuleStartup = true
}

// add EnsureHooks task for every enabled module, right after EnsureCRDs
if s.moduleManager.ModuleHasConversionWebhooks(moduleName) {
resultingTasks = append(resultingTasks, s.newEnsureHooksTask(moduleName, newLogLabels, queuedAt))
}

parallelRunMetadata.SetModuleMetadata(moduleName, task.ParallelRunModuleMetadata{
DoModuleStartup: doModuleStartup,
})
Expand Down Expand Up @@ -490,6 +495,11 @@ func (s *Task) CreateConvergeModulesTasks(state *module_manager.ModulesState, lo
doModuleStartup = true
}

// add EnsureHooks task for every enabled module, right after EnsureCRDs
if s.moduleManager.ModuleHasConversionWebhooks(modules[0]) {
resultingTasks = append(resultingTasks, s.newEnsureHooksTask(modules[0], newLogLabels, queuedAt))
}

newTask := sh_task.NewTask(task.ModuleRun).
WithLogLabels(newLogLabels).
WithQueueName("main").
Expand Down Expand Up @@ -541,6 +551,11 @@ func (s *Task) CreateConvergeModulesTasks(state *module_manager.ModulesState, lo
doModuleStartup = true
}

// add EnsureHooks task for every enabled module, right after EnsureCRDs
if s.moduleManager.ModuleHasConversionWebhooks(module) {
resultingTasks = append(resultingTasks, s.newEnsureHooksTask(module, newLogLabels, queuedAt))
}

schedulerRequests[idx] = &functional.Request{
Name: module,
Dependencies: deps[module],
Expand Down Expand Up @@ -570,6 +585,19 @@ func (s *Task) CreateConvergeModulesTasks(state *module_manager.ModulesState, lo
return resultingTasks
}

// newEnsureHooksTask builds a ModuleEnsureHooks task that applies the module's
// ConversionWebhook resources before its helm release.
func (s *Task) newEnsureHooksTask(moduleName string, logLabels map[string]string, queuedAt time.Time) sh_task.Task {
return sh_task.NewTask(task.ModuleEnsureHooks).
WithLogLabels(logLabels).
WithQueueName("main").
WithMetadata(task.HookMetadata{
EventDescription: "EnsureHooks",
ModuleName: moduleName,
IsReloadAll: true,
}).WithQueuedAt(queuedAt)
}

func (s *Task) IsStartupConvergeDone() bool {
return s.convergeState.GetFirstRunPhase() == converge.FirstDone
}
Expand Down
87 changes: 87 additions & 0 deletions pkg/task/tasks/module-ensure-hooks/task.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package moduleensurehooks

import (
"context"
"log/slog"
"time"

"github.com/deckhouse/deckhouse/pkg/log"
"go.opentelemetry.io/otel"

"github.com/flant/addon-operator/pkg"
"github.com/flant/addon-operator/pkg/module_manager"
"github.com/flant/addon-operator/pkg/task"
sh_task "github.com/flant/shell-operator/pkg/task"
"github.com/flant/shell-operator/pkg/task/queue"
)

const (
taskName = "module-ensure-hooks"
)

// TaskDependencies defines the interface for accessing necessary components.
type TaskDependencies interface {
GetModuleManager() *module_manager.ModuleManager
}

// RegisterTaskHandler creates a factory function for ModuleEnsureHooks tasks.
func RegisterTaskHandler(svc TaskDependencies) func(t sh_task.Task, logger *log.Logger) task.Task {
return func(t sh_task.Task, logger *log.Logger) task.Task {
return NewTask(
t,
svc.GetModuleManager(),
logger.Named("module-ensure-hooks"),
)
}
}

// Task applies a module's ConversionWebhook resources before its helm release.
type Task struct {
shellTask sh_task.Task

moduleManager *module_manager.ModuleManager

logger *log.Logger
}

// NewTask creates a new task handler for ensuring module conversion webhooks.
func NewTask(
shellTask sh_task.Task,
moduleManager *module_manager.ModuleManager,
logger *log.Logger,
) *Task {
return &Task{
shellTask: shellTask,
moduleManager: moduleManager,
logger: logger,
}
}

// Handle renders and applies the module's ConversionWebhook resources.
func (s *Task) Handle(ctx context.Context) queue.TaskResult {
_, span := otel.Tracer(taskName).Start(ctx, "handle")
defer span.End()

hm := task.HookMetadataAccessor(s.shellTask)

res := queue.TaskResult{
Status: queue.Success,
}

baseModule := s.moduleManager.GetModule(hm.ModuleName)

s.logger.Debug("Module ensureHooks", slog.String(pkg.LogKeyName, hm.ModuleName))

if err := s.moduleManager.EnsureConversionWebhooks(hm.ModuleName, s.shellTask.GetLogLabels()); err != nil {
s.moduleManager.UpdateModuleLastErrorAndNotify(baseModule, err)

s.logger.Error("ModuleEnsureHooks failed.", log.Err(err))

s.shellTask.UpdateFailureMessage(err.Error())
s.shellTask.WithQueuedAt(time.Now())

res.Status = queue.Fail
}

return res
}
Loading