├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── config.yml │ └── feature.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yaml ├── release.yml └── workflows │ ├── build.yaml │ ├── checks.yaml │ ├── docs.yaml │ ├── lint.yaml │ ├── publish-dev.yaml │ ├── publish-release.yaml │ └── tests.yaml ├── .gitignore ├── .gitmodules ├── .golangci.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── addon-operator │ └── main.go └── post-renderer │ └── main.go ├── docs ├── book.toml └── src │ ├── HOOKS.md │ ├── LIFECYCLE-STEPS.md │ ├── LIFECYCLE.md │ ├── MODULES.md │ ├── OVERVIEW.md │ ├── README.md │ ├── RUNNING.md │ ├── SUMMARY.md │ ├── VALUES.md │ ├── image │ ├── global_values_flow.png │ ├── logo-addon-operator-small.png │ ├── module_values_flow.png │ ├── readme-1.gif │ ├── readme-2.gif │ ├── readme-3.gif │ ├── readme-4.gif │ ├── readme-5.gif │ └── readme-6.gif │ └── metrics │ ├── METRICS_FROM_HOOKS.md │ ├── ROOT.md │ └── SELF_METRICS.md ├── examples ├── 001-startup-global │ ├── Dockerfile │ ├── README.md │ ├── addon-operator-cm.yaml │ ├── addon-operator-pod.yaml │ ├── addon-operator-rbac.yaml │ ├── global-hooks │ │ └── hook.sh │ └── modules │ │ └── README.md ├── 002-startup-global-high-availability │ ├── Dockerfile │ ├── README.md │ ├── addon-operator-cm.yaml │ ├── addon-operator-deployment.yaml │ ├── addon-operator-rbac.yaml │ ├── global-hooks │ │ └── hook.sh │ └── modules │ │ └── README.md ├── 101-module-sysctl-tuner │ ├── Dockerfile │ ├── README.md │ ├── addon-operator-cm.yaml │ ├── addon-operator-pod.yaml │ ├── addon-operator-rbac.yaml │ └── modules │ │ └── 001-sysctl-tuner │ │ ├── Chart.yaml │ │ ├── README.md │ │ ├── hooks │ │ └── module-hooks.sh │ │ ├── templates │ │ └── daemon-set.yaml │ │ └── values.yaml ├── 201-sysctl-tuner-values │ ├── Dockerfile │ ├── README.md │ ├── addon-operator-cm.yaml │ ├── addon-operator-deploy.yaml │ ├── addon-operator-rbac.yaml │ └── modules │ │ └── 001-sysctl-tuner │ │ ├── Chart.yaml │ │ ├── README.md │ │ ├── hooks │ │ └── module-hooks.sh │ │ ├── templates │ │ └── daemon-set.yaml │ │ └── values.yaml ├── 202-module-symlinks │ ├── Dockerfile │ ├── README.md │ ├── deploy │ │ ├── cm.yaml │ │ ├── deployment.yaml │ │ └── rbac.yaml │ ├── global-hooks │ │ └── .gitkeep │ ├── module_dir_1 │ │ └── 001-backend │ │ │ ├── Chart.yaml │ │ │ ├── hooks │ │ │ └── startup │ │ │ ├── templates │ │ │ └── backend.yaml │ │ │ └── values.yaml │ ├── module_dir_2 │ │ └── 002-frontend │ └── module_storage │ │ └── 002-frontend │ │ ├── Chart.yaml │ │ ├── hooks │ │ └── startup │ │ ├── templates │ │ └── frontend.yaml │ │ └── values.yaml └── 700-go-hook │ ├── README.md │ ├── global-hooks │ └── global-go-hook.go │ ├── modules │ └── 001-module-go-hooks │ │ └── hooks │ │ └── go_hooks.go │ └── register_go_hooks.go.tpl ├── go.mod ├── go.sum ├── pkg ├── addon-operator │ ├── admission_http_server.go │ ├── bootstrap.go │ ├── converge │ │ └── converge.go │ ├── debug_server.go │ ├── ensure_crds.go │ ├── handler_manager_events.go │ ├── handler_module_manager.go │ ├── http_server.go │ ├── kube_client.go │ ├── metrics.go │ ├── operator.go │ ├── operator_test.go │ ├── queue.go │ ├── queue_test.go │ └── testdata │ │ ├── converge__main_queue_only │ │ ├── config_map.yaml │ │ ├── global-hooks │ │ │ ├── hook01_startup_20_kube.sh │ │ │ ├── hook02_startup_1_schedule.sh │ │ │ └── hook03_startup_10_kube_schedule.sh │ │ └── modules │ │ │ ├── 000-module-alpha │ │ │ ├── Chart.yaml │ │ │ └── hooks │ │ │ │ ├── hook01_startup_20_kube.sh │ │ │ │ └── hook02_startup_1_schedule.sh │ │ │ └── 001-module-beta │ │ │ ├── Chart.yaml │ │ │ └── hooks │ │ │ ├── hook01_startup_20_kube.sh │ │ │ └── hook02_after_delete_helm.sh │ │ ├── log_task__wait_for_synchronization │ │ ├── config_map.yaml │ │ ├── global-hooks │ │ │ └── hook01_startup_20_kube.sh │ │ └── modules │ │ │ └── 000-module-alpha │ │ │ ├── Chart.yaml │ │ │ └── hooks │ │ │ └── hook01_startup_20_kube.sh │ │ └── startup_tasks │ │ └── global-hooks │ │ ├── hook01_startup_20_kube.sh │ │ ├── hook02_startup_1_schedule.sh │ │ └── hook03_startup_10_kube_schedule.sh ├── app │ ├── app.go │ └── debug-cmd.go ├── helm │ ├── client │ │ └── client.go │ ├── helm.go │ ├── helm3lib │ │ ├── helm3lib.go │ │ ├── helm3lib_test.go │ │ └── testdata │ │ │ └── chart │ │ │ ├── Chart.yaml │ │ │ └── templates │ │ │ └── 001-resources.yaml │ ├── helm_test.go │ ├── post_renderer │ │ ├── post_renderer.go │ │ └── post_renderer_test.go │ ├── test │ │ └── mock │ │ │ └── mock.go │ ├── version_detect.go │ └── version_detect_test.go ├── helm_resources_manager │ ├── helm_resources_manager.go │ ├── resources_monitor.go │ ├── test │ │ └── mock │ │ │ └── mock.go │ └── types │ │ └── types.go ├── hook │ └── types │ │ └── bindings.go ├── kube_config_manager │ ├── backend │ │ ├── backend.go │ │ └── configmap │ │ │ ├── configmap.go │ │ │ └── configmap_test.go │ ├── checksums.go │ ├── checksums_test.go │ ├── config │ │ ├── config.go │ │ └── event.go │ ├── kube_config_manager.go │ └── kube_config_manager_test.go ├── labels.go ├── module_manager │ ├── environment_manager │ │ ├── evironment_manager.go │ │ ├── evironment_manager_test.go │ │ ├── mount.go │ │ └── mount_linux.go │ ├── go_hook │ │ ├── filter_result.go │ │ ├── filter_result_test.go │ │ ├── go_hook.go │ │ ├── logger.go │ │ └── metrics │ │ │ └── collector.go │ ├── loader │ │ ├── fs │ │ │ ├── fs.go │ │ │ ├── fs_test.go │ │ │ └── testdata │ │ │ │ └── module_loader │ │ │ │ ├── dir1 │ │ │ │ ├── 001-module-one │ │ │ │ ├── 002-module-two │ │ │ │ └── values.yaml │ │ │ │ ├── dir2 │ │ │ │ ├── 012-mod-two │ │ │ │ ├── 100-mod-one │ │ │ │ └── values.yaml │ │ │ │ ├── dir3 │ │ │ │ └── values.yaml │ │ │ │ └── modules │ │ │ │ ├── 001-module-one │ │ │ │ └── .gitkeep │ │ │ │ ├── 002-module-two │ │ │ │ └── .gitkeep │ │ │ │ └── 003-module-three │ │ │ │ └── .gitkeep │ │ └── loader.go │ ├── models │ │ ├── hooks │ │ │ ├── dependency.go │ │ │ ├── global_hook.go │ │ │ ├── global_hook_config.go │ │ │ ├── global_hook_test.go │ │ │ ├── kind │ │ │ │ ├── batch_hook.go │ │ │ │ ├── batch_hook_test.go │ │ │ │ ├── gohook.go │ │ │ │ ├── gohook_test.go │ │ │ │ ├── kind.go │ │ │ │ ├── shellhook.go │ │ │ │ └── shellhook_test.go │ │ │ ├── module_hook.go │ │ │ └── module_hook_config.go │ │ ├── modules │ │ │ ├── basic.go │ │ │ ├── basic_test.go │ │ │ ├── events │ │ │ │ └── events.go │ │ │ ├── global.go │ │ │ ├── helm.go │ │ │ ├── hook_storage.go │ │ │ ├── module_options.go │ │ │ ├── synchronization_state.go │ │ │ ├── testdata │ │ │ │ └── global │ │ │ │ │ └── openapi │ │ │ │ │ ├── config-values.yaml │ │ │ │ │ └── values.yaml │ │ │ ├── values_defaulting_transformers.go │ │ │ ├── values_layered.go │ │ │ ├── values_layered_test.go │ │ │ ├── values_storage.go │ │ │ └── values_storage_test.go │ │ └── moduleset │ │ │ ├── moduleset.go │ │ │ └── moduleset_test.go │ ├── module_manager.go │ ├── module_manager_hooks.go │ ├── module_manager_test.go │ ├── scheduler │ │ ├── extenders │ │ │ ├── dynamically_enabled │ │ │ │ ├── dynamic.go │ │ │ │ └── dynamic_test.go │ │ │ ├── error │ │ │ │ └── permanent.go │ │ │ ├── extenders.go │ │ │ ├── kube_config │ │ │ │ └── kube_config.go │ │ │ ├── mock │ │ │ │ └── extenders_mock.go │ │ │ ├── script_enabled │ │ │ │ ├── script.go │ │ │ │ ├── script_test.go │ │ │ │ └── testdata │ │ │ │ │ ├── 015-admission-policy-engine │ │ │ │ │ └── enabled │ │ │ │ │ ├── 020-node-local-dns │ │ │ │ │ └── enabled │ │ │ │ │ ├── 031-foo-bar │ │ │ │ │ └── enabled │ │ │ │ │ ├── 045-chrony │ │ │ │ │ └── enabled │ │ │ │ │ └── 402-ingress-nginx │ │ │ │ │ └── enabled │ │ │ └── static │ │ │ │ ├── static.go │ │ │ │ └── static_test.go │ │ ├── node │ │ │ ├── mock │ │ │ │ └── node_mock.go │ │ │ ├── node.go │ │ │ └── node_test.go │ │ ├── scheduler.go │ │ ├── scheduler_test.go │ │ └── testdata │ │ │ ├── 015-admission-policy-engine │ │ │ └── enabled │ │ │ ├── 042-kube-dns │ │ │ └── enabled │ │ │ ├── 133-foo-bar │ │ │ └── enabled │ │ │ ├── 20-cert-manager │ │ │ └── enabled │ │ │ ├── 30-openstack-cloud-provider │ │ │ └── enabled │ │ │ ├── 340-monitoring-applications │ │ │ └── enabled │ │ │ ├── 402-ingress-nginx │ │ │ └── enabled │ │ │ ├── 450-flant-integration │ │ │ └── enabled │ │ │ └── 909-test-echo │ │ │ └── enabled │ └── testdata │ │ ├── get__global_hook │ │ ├── global-hooks │ │ │ ├── 000-all-bindings │ │ │ │ └── hook │ │ │ └── 100-nested-hook │ │ │ │ └── sub │ │ │ │ └── sub │ │ │ │ └── hook │ │ └── modules │ │ │ └── .gitkeep │ │ ├── get__global_hooks_in_order │ │ ├── global-hooks │ │ │ └── 000-before-all-binding-hooks │ │ │ │ ├── a │ │ │ │ ├── b │ │ │ │ └── c │ │ └── modules │ │ │ └── .gitkeep │ │ ├── get__module │ │ └── modules │ │ │ └── 000-module │ │ │ └── .gitkeep │ │ ├── get__module_hook │ │ └── modules │ │ │ ├── 000-all-bindings │ │ │ └── hooks │ │ │ │ └── all-bindings │ │ │ └── 100-nested-hooks │ │ │ └── hooks │ │ │ └── sub │ │ │ └── sub │ │ │ └── nested-before-helm │ │ ├── get__module_hooks_in_order │ │ ├── config_map.yaml │ │ └── modules │ │ │ └── 107-after-helm-binding-hooks │ │ │ ├── hooks │ │ │ ├── a │ │ │ ├── b │ │ │ └── c │ │ │ └── values.yaml │ │ ├── load_and_validate_usage │ │ ├── global │ │ │ ├── hooks │ │ │ │ └── hook │ │ │ └── openapi │ │ │ │ └── config-values.yaml │ │ └── modules │ │ │ └── 000-test-module │ │ │ ├── hooks │ │ │ └── hook │ │ │ └── openapi │ │ │ └── config-values.yaml │ │ ├── load_values__common_and_module_and_kube │ │ ├── config_map.yaml │ │ └── modules │ │ │ ├── 000-with-values-1 │ │ │ └── values.yaml │ │ │ ├── 001-with-values-2 │ │ │ └── values.yaml │ │ │ ├── 002-without-values │ │ │ └── .gitkeep │ │ │ ├── 003-with-kube-values │ │ │ └── .gitkeep │ │ │ └── values.yaml │ │ ├── load_values__common_static_empty │ │ └── modules │ │ │ ├── .gitkeep │ │ │ └── 000-module │ │ │ └── .gitkeep │ │ ├── load_values__common_static_global_only │ │ └── modules │ │ │ └── values.yaml │ │ ├── load_values__module_apply_defaults │ │ ├── config_map.yaml │ │ ├── global-hooks │ │ │ └── openapi │ │ │ │ ├── config-values.yaml │ │ │ │ └── values.yaml │ │ └── modules │ │ │ ├── 000-module-one │ │ │ └── openapi │ │ │ │ ├── config-values.yaml │ │ │ │ └── values.yaml │ │ │ └── values.yaml │ │ ├── load_values__module_static_only │ │ └── modules │ │ │ ├── 000-with-values-1 │ │ │ └── values.yaml │ │ │ └── 001-with-values-2 │ │ │ └── values.yaml │ │ ├── loader │ │ └── values.yaml │ │ ├── modules_state__detect_cm_changes │ │ ├── config_map.yaml │ │ └── modules │ │ │ ├── 001-module-one │ │ │ └── .gitkeep │ │ │ ├── 003-module-three │ │ │ └── .gitkeep │ │ │ └── values.yaml │ │ ├── modules_state__no_cm__module_names_order │ │ └── modules │ │ │ ├── 000-module-c │ │ │ ├── .gitkeep │ │ │ └── values.yaml │ │ │ ├── 100-module-a │ │ │ └── .gitkeep │ │ │ ├── 200-module-b │ │ │ ├── .gitkeep │ │ │ └── values.yaml │ │ │ └── 300-module-disabled │ │ │ └── values.yaml │ │ ├── modules_state__no_cm__simple │ │ └── modules │ │ │ ├── 001-module-1 │ │ │ └── .gitkeep │ │ │ ├── 003-module-3 │ │ │ └── .gitkeep │ │ │ ├── 004-module-4 │ │ │ └── .gitkeep │ │ │ ├── 007-module-7 │ │ │ └── .gitkeep │ │ │ ├── 008-module-8 │ │ │ └── .gitkeep │ │ │ ├── 009-module-9 │ │ │ └── .gitkeep │ │ │ └── values.yaml │ │ ├── modules_state__no_cm__with_enabled_scripts │ │ └── modules │ │ │ ├── 001-alpha │ │ │ └── enabled │ │ │ ├── 002-beta │ │ │ └── enabled │ │ │ ├── 003-gamma │ │ │ └── enabled │ │ │ ├── 004-delta │ │ │ └── enabled │ │ │ ├── 005-epsilon │ │ │ └── enabled │ │ │ ├── 006-zeta │ │ │ └── enabled │ │ │ ├── 007-eta │ │ │ └── enabled │ │ │ └── values.yaml │ │ ├── test_delete_module │ │ └── modules │ │ │ └── 000-module │ │ │ ├── .helmignore │ │ │ ├── Chart.yaml │ │ │ ├── hooks │ │ │ ├── hook-1 │ │ │ └── hook-2 │ │ │ ├── templates │ │ │ ├── NOTES.txt │ │ │ ├── _helpers.tpl │ │ │ ├── deployment.yaml │ │ │ ├── ingress.yaml │ │ │ └── service.yaml │ │ │ └── values.yaml │ │ ├── test_run_global_hook │ │ ├── global-hooks │ │ │ ├── 000-update-kube-config │ │ │ │ └── merge_and_patch_values │ │ │ └── 100-update-dynamic │ │ │ │ └── merge_and_patch_values │ │ └── modules │ │ │ └── .gitkeep │ │ ├── test_run_module │ │ └── modules │ │ │ └── 000-module │ │ │ ├── .helmignore │ │ │ ├── Chart.yaml │ │ │ ├── hooks │ │ │ ├── hook-1 │ │ │ ├── hook-2 │ │ │ ├── hook-3 │ │ │ └── hook-4 │ │ │ ├── templates │ │ │ ├── NOTES.txt │ │ │ ├── _helpers.tpl │ │ │ ├── deployment.yaml │ │ │ ├── ingress.yaml │ │ │ └── service.yaml │ │ │ └── values.yaml │ │ └── test_run_module_hook │ │ └── modules │ │ ├── 000-update-kube-module-config │ │ └── hooks │ │ │ └── merge_and_patch_values │ │ └── 100-update-module-dynamic │ │ └── hooks │ │ └── merge_and_patch_values ├── task │ ├── apply-kube-config-values │ │ └── task.go │ ├── converge-modules │ │ └── task.go │ ├── discover-crds │ │ └── service.go │ ├── discover-helm-release │ │ └── task.go │ ├── global-hook-enable-kubernetes-bindings │ │ └── task.go │ ├── global-hook-enable-schedule-bindings │ │ └── task.go │ ├── global-hook-run │ │ └── task.go │ ├── global-hook-wait-kubernetes-synchronization │ │ └── task.go │ ├── helpers │ │ └── helpers.go │ ├── hook_metadata.go │ ├── module-delete │ │ └── task.go │ ├── module-ensure-crds │ │ └── task.go │ ├── module-hook-run │ │ └── task.go │ ├── module-purge │ │ └── task.go │ ├── module-run │ │ └── task.go │ ├── parallel-module-run │ │ └── task.go │ ├── parallel │ │ └── parallel.go │ ├── queue │ │ └── queue.go │ ├── service │ │ ├── converge.go │ │ ├── logs.go │ │ ├── metric.go │ │ └── service.go │ ├── task.go │ └── test │ │ └── task_metadata_test.go ├── utils │ ├── chroot.go │ ├── fschecksum.go │ ├── fswalk.go │ ├── helpers.go │ ├── jsonpatch.go │ ├── loader.go │ ├── merge_labels.go │ ├── mergemap.go │ ├── mergemap_test.go │ ├── module_config.go │ ├── module_config_test.go │ ├── module_list.go │ ├── stdliblogtolog │ │ ├── adapter.go │ │ └── adapter_test.go │ ├── values.go │ ├── values_patch.go │ ├── values_patch_test.go │ └── values_test.go └── values │ └── validation │ ├── cel │ └── cel.go │ ├── defaulting.go │ ├── defaulting_test.go │ ├── extend_test.go │ ├── required_test.go │ ├── schema │ ├── additional-properties.go │ ├── copy.go │ ├── extend.go │ ├── required-for-helm.go │ └── transform.go │ ├── schemas.go │ ├── schemas_test.go │ ├── testdata │ ├── test-schema-bad.yaml │ ├── test-schema-ok-project-2.yaml │ ├── test-schema-ok-project.yaml │ └── test-schema-ok.yaml │ └── validator_test.go └── sdk ├── registry.go ├── registry_test.go ├── sdk.go └── test ├── sdk_test.go └── simple_operator ├── global-hooks └── go-hook.go └── modules ├── 001-module-one └── hooks │ └── module-one-hook.go └── 002-module-two └── hooks └── level1 └── sublevel └── sub-sub-hook.go /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: Report a bug to help us improve Addon-operator 4 | --- 5 | 11 | 12 | **Expected behavior (what you expected to happen)**: 13 | 14 | **Actual behavior (what actually happened)**: 15 | 16 | **Steps to reproduce**: 17 | 1. ... 18 | 2. ... 19 | 3. ... 20 | 21 | **Environment**: 22 | - Addon-operator version: 23 | - Helm version: 24 | - Kubernetes version: 25 | - Installation type (kubectl apply, helm chart, etc.): 26 | 27 | **Anything else we should know?**: 28 | 29 | **Additional information for debugging (if necessary)**: 30 | 31 |
Hook script
32 | 
33 | 
34 | 35 |
Logs
36 | 
37 | 
38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💬 Github Discussions 4 | url: https://github.com/flant/addon-operator/discussions 5 | about: Please ask and answer questions here 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature request 3 | about: Suggest an idea for Addon-operator 4 | --- 5 | 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the solution you'd like to see** 14 | 15 | 16 | **Describe alternatives you've considered** 17 | 18 | 19 | **Additional context** 20 | 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | #### Overview 10 | 11 | 12 | 13 | #### What this PR does / why we need it 14 | 15 | 21 | 22 | #### Special notes for your reviewer 23 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | # Maintain Go modules 9 | - package-ecosystem: "gomod" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | day: "monday" 14 | open-pull-requests-limit: 1 15 | ignore: 16 | - dependency-name: "k8s.io/api" 17 | - dependency-name: "k8s.io/apimachinery" 18 | - dependency-name: "k8s.io/client-go" 19 | 20 | - package-ecosystem: "docker" 21 | directory: "/" 22 | schedule: 23 | interval: "weekly" 24 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - release-note/ignore 5 | categories: 6 | - title: Exciting New Features 🎉 7 | labels: 8 | - release-note/new-feature 9 | - title: Enhancements 🚀 10 | labels: 11 | - enhancement 12 | - release-note/enhancement 13 | - title: Bug Fixes 🐛 14 | labels: 15 | - bug 16 | - release-note/bug 17 | - title: Breaking Changes 🛠 18 | labels: 19 | - release-note/breaking-change 20 | - title: Deprecations ❌ 21 | labels: 22 | - release-note/deprecation 23 | - title: Dependency Updates ⬆️ 24 | labels: 25 | - dependencies 26 | - release-note/dependencies 27 | - title: Other Changes 28 | labels: 29 | - "*" 30 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | # every push to a branch: build a binary 2 | name: Build 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | 7 | jobs: 8 | build_binary: 9 | name: Build addon-operator binary 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set up Go 1.23 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: '1.23' 16 | id: go 17 | 18 | - name: Check out addon-operator code 19 | uses: actions/checkout@v4 20 | 21 | - name: Restore Go modules 22 | id: go-modules-cache 23 | uses: actions/cache@v4 24 | with: 25 | path: | 26 | ~/go/pkg/mod 27 | key: ${{ runner.os }}-gomod-${{ hashFiles('go.mod', 'go.sum') }} 28 | restore-keys: | 29 | ${{ runner.os }}-gomod- 30 | 31 | - name: Download Go modules 32 | if: steps.go-modules-cache.outputs.cache-hit != 'true' 33 | run: | 34 | go mod download 35 | echo -n "Go modules unpacked size is: " && du -sh $HOME/go/pkg/mod 36 | 37 | - name: Build binary 38 | run: | 39 | export GOOS=linux 40 | 41 | go build ./cmd/addon-operator -------------------------------------------------------------------------------- /.github/workflows/checks.yaml: -------------------------------------------------------------------------------- 1 | name: PR Checks 2 | 3 | on: 4 | pull_request: 5 | types: [opened, labeled, unlabeled, synchronize] 6 | 7 | jobs: 8 | release-label: 9 | name: Release note label 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check minimum labels 14 | uses: mheap/github-action-required-labels@v5 15 | with: 16 | mode: minimum 17 | count: 1 18 | labels: "release-note/dependencies, dependencies, release-note/deprecation, release-note/breaking-change, release-note/bug, bug, release-note/enhancement, enhancement, release-note/new-feature, release-note/ignore" 19 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-20.04 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Setup mdBook 18 | uses: peaceiris/actions-mdbook@v2 19 | with: 20 | mdbook-version: '0.4.10' 21 | 22 | - run: mdbook build docs 23 | 24 | - name: Deploy 25 | uses: peaceiris/actions-gh-pages@v4 26 | if: ${{ github.ref == 'refs/heads/main' }} 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | publish_dir: ./docs/book 30 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | # every push to a branch: 2 | # - Run Go linters. 3 | # - Check grammar with codespell. 4 | name: Lint 5 | on: 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | go_linters: 11 | name: Run Go linters 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set up Go 1.23 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: '1.23' 18 | id: go 19 | 20 | - name: Check out addon-operator code 21 | uses: actions/checkout@v4 22 | 23 | - name: Restore Go modules 24 | id: go-modules-cache 25 | uses: actions/cache@v4 26 | with: 27 | path: | 28 | ~/go/pkg/mod 29 | key: ${{ runner.os }}-gomod-${{ hashFiles('go.mod', 'go.sum') }} 30 | restore-keys: | 31 | ${{ runner.os }}-gomod- 32 | 33 | - name: Download Go modules 34 | if: steps.go-modules-cache.outputs.cache-hit != 'true' 35 | run: | 36 | go mod download 37 | echo -n "Go modules unpacked size is: " && du -sh $HOME/go/pkg/mod 38 | 39 | - name: Run golangci-lint 40 | run: | 41 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b . v2.1.6 42 | ./golangci-lint run 43 | 44 | codespell: 45 | name: Run codespell 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/setup-python@v5 49 | with: 50 | python-version: 3.8 51 | 52 | - name: Check out addon-operator code 53 | uses: actions/checkout@v4 54 | 55 | - name: Run codespell 56 | run: | 57 | pip install codespell==v1.17.1 58 | codespell --skip=".git,go.mod,go.sum,*.log,*.gif,*.png" -L witht,eventtypes,uint,uptodate,afterall 59 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | # Run unit tests. 2 | # Starts for new and updated pull requests. 3 | name: Unit tests 4 | on: 5 | pull_request: 6 | types: [opened, synchronize] 7 | 8 | jobs: 9 | run_unit_tests: 10 | name: Run unit tests 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Set up Go 1.23 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version: '1.23' 17 | id: go 18 | 19 | - name: Check out addon-operator code 20 | uses: actions/checkout@v4 21 | 22 | - name: Restore Go modules 23 | id: go-modules-cache 24 | uses: actions/cache@v4 25 | with: 26 | path: | 27 | ~/go/pkg/mod 28 | key: ${{ runner.os }}-gomod-${{ hashFiles('go.mod', 'go.sum') }} 29 | restore-keys: | 30 | ${{ runner.os }}-gomod- 31 | 32 | - name: Download Go modules 33 | if: steps.go-modules-cache.outputs.cache-hit != 'true' 34 | run: | 35 | go mod download 36 | echo -n "Go modules unpacked size is: " && du -sh $HOME/go/pkg/mod 37 | 38 | - name: Run unit tests 39 | run: | 40 | export GOOS=linux 41 | 42 | go test \ 43 | --race \ 44 | ./cmd/... ./pkg/... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Graph images 9 | *.gv 10 | *gv.svg 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # binary produced by go build 19 | /addon-operator 20 | 21 | .idea 22 | libjq 23 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/.gitmodules -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Prebuilt libjq. 2 | FROM --platform=${TARGETPLATFORM:-linux/amd64} flant/jq:b6be13d5-musl as libjq 3 | 4 | # Go builder. 5 | 6 | FROM --platform=${TARGETPLATFORM:-linux/amd64} golang:1.23-alpine AS builder 7 | 8 | 9 | ARG appVersion=latest 10 | RUN apk --no-cache add git ca-certificates gcc musl-dev libc-dev binutils-gold 11 | 12 | # Cache-friendly download of go dependencies. 13 | ADD go.mod go.sum /app/ 14 | WORKDIR /app 15 | RUN go mod download 16 | 17 | COPY --from=libjq /libjq /libjq 18 | ADD . /app 19 | 20 | # Clone shell-operator to get frameworks 21 | RUN git clone https://github.com/flant/shell-operator shell-operator-clone && \ 22 | cd shell-operator-clone && \ 23 | git checkout v1.7.2 24 | 25 | RUN shellOpVer=$(go list -m all | grep shell-operator | cut -d' ' -f 2-) \ 26 | GOOS=linux \ 27 | go build -ldflags="-linkmode external -extldflags '-static' -s -w -X 'github.com/flant/shell-operator/pkg/app.Version=$shellOpVer' -X 'github.com/flant/addon-operator/pkg/app.Version=$appVersion'" \ 28 | -o addon-operator \ 29 | ./cmd/addon-operator 30 | 31 | # Build helm post-renderer binary (required if helm3 binary is in use) 32 | RUN GOOS=linux \ 33 | go build -o post-renderer ./cmd/post-renderer 34 | 35 | # Final image 36 | FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine:3.21 37 | ARG TARGETPLATFORM 38 | # kubectl url has no variant (v7) 39 | # helm url has dashes and no variant (v7) 40 | RUN apk --no-cache add ca-certificates bash sed tini && \ 41 | kubectlArch=$(echo ${TARGETPLATFORM:-linux/amd64} | sed 's/\/v7//') && \ 42 | echo "Download kubectl for ${kubectlArch}" && \ 43 | wget https://storage.googleapis.com/kubernetes-release/release/v1.25.5/bin/${kubectlArch}/kubectl -O /bin/kubectl && \ 44 | chmod +x /bin/kubectl && \ 45 | helmArch=$(echo ${TARGETPLATFORM:-linux/amd64} | sed 's/\//-/g;s/-v7//') && \ 46 | wget https://get.helm.sh/helm-v3.10.3-${helmArch}.tar.gz -O /helm.tgz && \ 47 | tar -z -x -C /bin -f /helm.tgz --strip-components=1 ${helmArch}/helm && \ 48 | rm -f /helm.tgz 49 | COPY --from=libjq /bin/jq /usr/bin 50 | COPY --from=builder /app/addon-operator / 51 | COPY --from=builder /app/post-renderer / 52 | COPY --from=builder /app/shell-operator-clone/frameworks/shell/ /framework/shell/ 53 | COPY --from=builder /app/shell-operator-clone/shell_lib.sh / 54 | 55 | WORKDIR / 56 | 57 | RUN mkdir /global-hooks /modules 58 | ENV MODULES_DIR /modules 59 | ENV GLOBAL_HOOKS_DIR /global-hooks 60 | ENTRYPOINT ["/sbin/tini", "--", "/addon-operator"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO=$(shell which go) 2 | GIT=$(shell which git) 3 | 4 | .PHONY: go-check 5 | go-check: 6 | $(call error-if-empty,$(GO),go) 7 | 8 | .PHONY: git-check 9 | git-check: 10 | $(call error-if-empty,$(GIT),git) 11 | 12 | .PHONY: go-module-version 13 | go-module-version: go-check git-check 14 | @echo "go get $(shell $(GO) list ./cmd/addon-operator)@$(shell $(GIT) rev-parse HEAD)" 15 | 16 | .PHONY: test 17 | test: go-check 18 | @$(GO) test --race --cover ./... 19 | 20 | define error-if-empty 21 | @if [[ -z $(1) ]]; then echo "$(2) not installed"; false; fi 22 | endef -------------------------------------------------------------------------------- /cmd/post-renderer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/flant/addon-operator/pkg/helm/post_renderer" 10 | ) 11 | 12 | func main() { 13 | inputBytes, err := io.ReadAll(os.Stdin) 14 | if err != nil { 15 | fmt.Fprintf(os.Stderr, "couldn't read input from stdin: %s", err) 16 | os.Exit(1) 17 | } 18 | buf := bytes.NewBuffer(inputBytes) 19 | 20 | renderer := post_renderer.NewPostRenderer(map[string]string{ 21 | "heritage": "addon-operator", 22 | }) 23 | 24 | outputBytes, err := renderer.Run(buf) 25 | if err != nil { 26 | fmt.Fprintf(os.Stderr, "could not render input from stdin: %s", err) 27 | os.Exit(1) 28 | } 29 | 30 | if _, err := os.Stdout.Write(outputBytes.Bytes()); err != nil { 31 | fmt.Fprintf(os.Stderr, "could not write rendered output to stdout: %s", err) 32 | os.Exit(1) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["The Addon-Operator Maintainers"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Addon-operator" 7 | 8 | [output.html] 9 | curly-quotes = true 10 | git-repository-url = "https://github.com/flant/addon-operator" 11 | -------------------------------------------------------------------------------- /docs/src/README.md: -------------------------------------------------------------------------------- 1 |

2 | addon-operator logo 3 |

4 | 5 |

6 | docker pull flant/addon-operator 7 | GH Discussions 8 | Telegram chat RU 9 |

10 | 11 | # Installation 12 | 13 | You may use a prepared image [flant/addon-operator][docker-hub] to install addon-operator in a cluster. The image comprises a binary `addon-operator` file as well as several required tools: `helm`, `kubectl`, `jq`, `bash`. 14 | 15 | The installation incorporates the image building process with *files of modules and hooks*, applying the necessary RBAC rights and deploying the image in the cluster. 16 | 17 | ## Examples 18 | 19 | To experiment with modules, hooks, and values we've prepared some [examples][examples]. 20 | 21 | [Deckhouse Kubernetes Platform][deckhouse] was an initial reason to create addon-operator, thus [its modules][deckhouse-modules] might become a vital source of inspiration for implementing your own modules. 22 | 23 | Sharing your examples of using addon-operator is much appreciated. Please, use the [relevant Discussions section][discussions-sharing] for that. 24 | 25 | # Community 26 | 27 | Please feel free to reach developers/maintainers and users via [GitHub Discussions][discussions] for any questions regarding addon-operator. 28 | 29 | You're also welcome to follow [@flant_com][twitter] to stay informed about all our Open Source initiatives. 30 | 31 | # License 32 | 33 | Apache License 2.0, see [LICENSE][license]. 34 | 35 | [deckhouse]: https://deckhouse.io/ 36 | [deckhouse-modules]: https://github.com/deckhouse/deckhouse/tree/main/modules 37 | [discussions]: https://github.com/flant/addon-operator/discussions 38 | [discussions-sharing]: https://github.com/flant/addon-operator/discussions/categories/show-and-tell 39 | [docker-hub]: https://hub.docker.com/r/flant/addon-operator 40 | [examples]: https://github.com/flant/addon-operator/tree/main/examples 41 | [license]: https://github.com/flant/addon-operator/blob/main/LICENSE 42 | [twitter]: https://twitter.com/flant_com 43 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Introduction](README.md) 4 | 5 | - [Overview](OVERVIEW.md) 6 | - [Running Addon-operator](RUNNING.md) 7 | - [Lifecycle](LIFECYCLE.md) 8 | - [Steps](LIFECYCLE-STEPS.md) 9 | - [Modules](MODULES.md) 10 | - [Hooks](HOOKS.md) 11 | - [Values](VALUES.md) 12 | - [Metrics](metrics/ROOT.md) 13 | - [Self metrics](metrics/SELF_METRICS.md) 14 | - [Return metrics from hooks](metrics/METRICS_FROM_HOOKS.md) 15 | -------------------------------------------------------------------------------- /docs/src/image/global_values_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/docs/src/image/global_values_flow.png -------------------------------------------------------------------------------- /docs/src/image/logo-addon-operator-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/docs/src/image/logo-addon-operator-small.png -------------------------------------------------------------------------------- /docs/src/image/module_values_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/docs/src/image/module_values_flow.png -------------------------------------------------------------------------------- /docs/src/image/readme-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/docs/src/image/readme-1.gif -------------------------------------------------------------------------------- /docs/src/image/readme-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/docs/src/image/readme-2.gif -------------------------------------------------------------------------------- /docs/src/image/readme-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/docs/src/image/readme-3.gif -------------------------------------------------------------------------------- /docs/src/image/readme-4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/docs/src/image/readme-4.gif -------------------------------------------------------------------------------- /docs/src/image/readme-5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/docs/src/image/readme-5.gif -------------------------------------------------------------------------------- /docs/src/image/readme-6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/docs/src/image/readme-6.gif -------------------------------------------------------------------------------- /docs/src/metrics/ROOT.md: -------------------------------------------------------------------------------- 1 | # Addon-operator metrics 2 | 3 | The Addon-operator implements Prometheus target at `/metrics` endpoint. The default port is `9650`. 4 | -------------------------------------------------------------------------------- /examples/001-startup-global/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM flant/addon-operator:latest 2 | ADD modules /modules 3 | ADD global-hooks /global-hooks 4 | -------------------------------------------------------------------------------- /examples/001-startup-global/README.md: -------------------------------------------------------------------------------- 1 | ## onStartup global hooks example 2 | 3 | Example of a global hook written as bash script. 4 | 5 | ### run 6 | 7 | Build addon-operator image with custom scripts: 8 | 9 | ``` 10 | docker build -t "registry.mycompany.com/addon-operator:startup-global" . 11 | docker push registry.mycompany.com/addon-operator:startup-global 12 | ``` 13 | 14 | Edit image in addon-operator-pod.yaml and apply manifests: 15 | 16 | ``` 17 | kubectl create ns example-startup-global 18 | kubectl -n example-startup-global apply -f addon-operator-rbac.yaml 19 | kubectl -n example-startup-global apply -f addon-operator-pod.yaml 20 | ``` 21 | 22 | See in logs that hook.sh was run at startup: 23 | 24 | ``` 25 | kubectl -n example-startup-global logs pod/addon-operator -f 26 | ... 27 | INFO : Initializing global hooks ... 28 | INFO : INIT: global hook 'hook.sh' ... 29 | ... 30 | INFO : TASK_RUN GlobalHookRun@ON_STARTUP hook.sh 31 | INFO : Running global hook 'hook.sh' binding 'ON_STARTUP' ... 32 | OnStartup global hook 33 | ... 34 | ``` 35 | 36 | ### cleanup 37 | 38 | ``` 39 | kubectl delete clusterrolebinding/addon-operator 40 | kubectl delete clusterrole/addon-operator 41 | kubectl delete ns/example-startup-global 42 | docker rmi registry.mycompany.com/addon-operator:startup-global 43 | ``` 44 | -------------------------------------------------------------------------------- /examples/001-startup-global/addon-operator-cm.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: addon-operator 6 | data: 7 | global: "" 8 | -------------------------------------------------------------------------------- /examples/001-startup-global/addon-operator-pod.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: addon-operator 6 | spec: 7 | containers: 8 | - name: addon-operator 9 | image: registry.mycompany.com/addon-operator:startup-global 10 | imagePullPolicy: Always 11 | env: 12 | - name: ADDON_OPERATOR_NAMESPACE 13 | valueFrom: 14 | fieldRef: 15 | fieldPath: metadata.namespace 16 | serviceAccountName: addon-operator-acc 17 | -------------------------------------------------------------------------------- /examples/001-startup-global/addon-operator-rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: addon-operator-acc 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRole 9 | metadata: 10 | name: addon-operator 11 | rules: 12 | - apiGroups: 13 | - "*" 14 | resources: 15 | - "*" 16 | verbs: 17 | - "*" 18 | - nonResourceURLs: 19 | - "*" 20 | verbs: 21 | - "*" 22 | --- 23 | apiVersion: rbac.authorization.k8s.io/v1 24 | kind: ClusterRoleBinding 25 | metadata: 26 | name: addon-operator 27 | roleRef: 28 | apiGroup: rbac.authorization.k8s.io 29 | kind: ClusterRole 30 | name: addon-operator 31 | subjects: 32 | - kind: ServiceAccount 33 | name: addon-operator-acc 34 | namespace: default 35 | -------------------------------------------------------------------------------- /examples/001-startup-global/global-hooks/hook.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ $1 == "--config" ]] ; then 4 | echo '{"configVersion":"v1", "onStartup": 1}' 5 | else 6 | echo "OnStartup global shell hook" 7 | fi 8 | -------------------------------------------------------------------------------- /examples/001-startup-global/modules/README.md: -------------------------------------------------------------------------------- 1 | > FIXME: `modules` directory is required even if only global-hooks are used 2 | -------------------------------------------------------------------------------- /examples/002-startup-global-high-availability/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM flant/addon-operator:latest 2 | ADD modules /modules 3 | ADD global-hooks /global-hooks 4 | -------------------------------------------------------------------------------- /examples/002-startup-global-high-availability/README.md: -------------------------------------------------------------------------------- 1 | ## onStartup global hooks example 2 | 3 | Example of a global hook written as bash script. 4 | 5 | ### run 6 | 7 | Build addon-operator image with custom scripts: 8 | 9 | ``` 10 | docker build -t "registry.mycompany.com/addon-operator:startup-global" . 11 | docker push registry.mycompany.com/addon-operator:startup-global 12 | ``` 13 | 14 | Edit image in addon-operator-pod.yaml and apply manifests: 15 | 16 | ``` 17 | kubectl create ns example-startup-global 18 | kubectl -n example-startup-global apply -f addon-operator-rbac.yaml 19 | kubectl -n example-startup-global apply -f addon-operator-pod.yaml 20 | ``` 21 | 22 | See in logs that hook.sh was run at startup: 23 | 24 | ``` 25 | kubectl -n example-startup-global logs pod/addon-operator -f 26 | ... 27 | INFO : Initializing global hooks ... 28 | INFO : INIT: global hook 'hook.sh' ... 29 | ... 30 | INFO : TASK_RUN GlobalHookRun@ON_STARTUP hook.sh 31 | INFO : Running global hook 'hook.sh' binding 'ON_STARTUP' ... 32 | OnStartup global hook 33 | ... 34 | ``` 35 | 36 | ### cleanup 37 | 38 | ``` 39 | kubectl delete clusterrolebinding/addon-operator 40 | kubectl delete clusterrole/addon-operator 41 | kubectl delete ns/example-startup-global 42 | docker rmi registry.mycompany.com/addon-operator:startup-global 43 | ``` 44 | -------------------------------------------------------------------------------- /examples/002-startup-global-high-availability/addon-operator-cm.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: addon-operator 6 | data: 7 | global: "" 8 | -------------------------------------------------------------------------------- /examples/002-startup-global-high-availability/addon-operator-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | name: addon-operator 6 | spec: 7 | replicas: 2 8 | selector: 9 | matchLabels: 10 | app: addon-operator 11 | strategy: 12 | rollingUpdate: 13 | maxSurge: 25% 14 | maxUnavailable: 1 15 | type: RollingUpdate 16 | template: 17 | metadata: 18 | labels: 19 | app: addon-operator 20 | spec: 21 | affinity: 22 | podAntiAffinity: 23 | preferredDuringSchedulingIgnoredDuringExecution: 24 | - podAffinityTerm: 25 | labelSelector: 26 | matchExpressions: 27 | - key: app 28 | operator: In 29 | values: 30 | - addon-operator 31 | topologyKey: kubernetes.io/hostname 32 | weight: 100 33 | containers: 34 | - env: 35 | - name: ADDON_OPERATOR_POD 36 | valueFrom: 37 | fieldRef: 38 | apiVersion: v1 39 | fieldPath: metadata.name 40 | - name: ADDON_OPERATOR_HA 41 | value: "true" 42 | - name: ADDON_OPERATOR_NAMESPACE 43 | valueFrom: 44 | fieldRef: 45 | apiVersion: v1 46 | fieldPath: metadata.namespace 47 | - name: ADDON_OPERATOR_LISTEN_ADDRESS 48 | valueFrom: 49 | fieldRef: 50 | apiVersion: v1 51 | fieldPath: status.podIP 52 | image: registry.mycompany.com/addon-operator:ha 53 | imagePullPolicy: IfNotPresent 54 | name: addon-operator 55 | readinessProbe: 56 | httpGet: 57 | path: /readyz 58 | port: 9650 59 | scheme: HTTP 60 | periodSeconds: 10 61 | successThreshold: 1 62 | timeoutSeconds: 1 63 | resources: {} 64 | terminationMessagePath: /dev/termination-log 65 | terminationMessagePolicy: File 66 | dnsPolicy: ClusterFirst 67 | restartPolicy: Always 68 | schedulerName: default-scheduler 69 | serviceAccount: addon-operator-acc 70 | serviceAccountName: addon-operator-acc 71 | terminationGracePeriodSeconds: 30 72 | -------------------------------------------------------------------------------- /examples/002-startup-global-high-availability/addon-operator-rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: addon-operator-acc 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRole 9 | metadata: 10 | name: addon-operator 11 | rules: 12 | - apiGroups: 13 | - "*" 14 | resources: 15 | - "*" 16 | verbs: 17 | - "*" 18 | - nonResourceURLs: 19 | - "*" 20 | verbs: 21 | - "*" 22 | --- 23 | apiVersion: rbac.authorization.k8s.io/v1 24 | kind: ClusterRoleBinding 25 | metadata: 26 | name: addon-operator 27 | roleRef: 28 | apiGroup: rbac.authorization.k8s.io 29 | kind: ClusterRole 30 | name: addon-operator 31 | subjects: 32 | - kind: ServiceAccount 33 | name: addon-operator-acc 34 | namespace: default 35 | -------------------------------------------------------------------------------- /examples/002-startup-global-high-availability/global-hooks/hook.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ $1 == "--config" ]] ; then 4 | echo '{"configVersion":"v1", "onStartup": 1}' 5 | else 6 | echo "OnStartup global shell hook" 7 | fi 8 | -------------------------------------------------------------------------------- /examples/002-startup-global-high-availability/modules/README.md: -------------------------------------------------------------------------------- 1 | > FIXME: `modules` directory is required even if only global-hooks are used 2 | -------------------------------------------------------------------------------- /examples/101-module-sysctl-tuner/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM flant/addon-operator:latest 2 | ADD modules /modules 3 | -------------------------------------------------------------------------------- /examples/101-module-sysctl-tuner/addon-operator-cm.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: addon-operator 6 | data: 7 | global: "" 8 | sysctlTuner: | 9 | {} 10 | -------------------------------------------------------------------------------- /examples/101-module-sysctl-tuner/addon-operator-pod.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: addon-operator 6 | spec: 7 | containers: 8 | - name: addon-operator 9 | image: registry.mycompany.com/addon-operator:module-sysctl-tuner 10 | imagePullPolicy: Always 11 | env: 12 | - name: ADDON_OPERATOR_NAMESPACE 13 | valueFrom: 14 | fieldRef: 15 | fieldPath: metadata.namespace 16 | serviceAccountName: addon-operator-acc 17 | -------------------------------------------------------------------------------- /examples/101-module-sysctl-tuner/addon-operator-rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: addon-operator-acc 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1beta1 8 | kind: ClusterRole 9 | metadata: 10 | name: addon-operator 11 | rules: 12 | - apiGroups: 13 | - "*" 14 | resources: 15 | - "*" 16 | verbs: 17 | - "*" 18 | - nonResourceURLs: 19 | - "*" 20 | verbs: 21 | - "*" 22 | --- 23 | apiVersion: rbac.authorization.k8s.io/v1beta1 24 | kind: ClusterRoleBinding 25 | metadata: 26 | name: addon-operator 27 | roleRef: 28 | apiGroup: rbac.authorization.k8s.io 29 | kind: ClusterRole 30 | name: addon-operator 31 | subjects: 32 | - kind: ServiceAccount 33 | name: addon-operator-acc 34 | namespace: example-module-sysctl-tuner 35 | -------------------------------------------------------------------------------- /examples/101-module-sysctl-tuner/modules/001-sysctl-tuner/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: sysctl-tuner 2 | version: 0.1.0 3 | -------------------------------------------------------------------------------- /examples/101-module-sysctl-tuner/modules/001-sysctl-tuner/README.md: -------------------------------------------------------------------------------- 1 | sysctl-tuner module 2 | =================== 3 | 4 | This module periodically applying systcl parametrs on nodes. 5 | 6 | Module is run in DaemonSet in privileged containers and apply 7 | parameters every 5 min. -------------------------------------------------------------------------------- /examples/101-module-sysctl-tuner/modules/001-sysctl-tuner/hooks/module-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # A stub hook just to make sure that events are handled properly. 4 | 5 | if [[ $1 == "--config" ]] ; then 6 | cat < ../module_storage/002-frontend 14 | ... 15 | ├── module_storage 16 | │ └── 002-frontend 17 | │ ├── Chart.yaml 18 | │ ├── templates 19 | │ │ ├── deploy-frontend.yaml 20 | ... 21 | ``` 22 | 23 | MODULES_DIR environment variable is used to specify two directories: 24 | 25 | ``` 26 | - name: MODULES_DIR 27 | value: "/module_dir_1:/module_dir_2" 28 | ``` 29 | 30 | ### Run 31 | 32 | Build addon-operator image with modules: 33 | 34 | ``` 35 | docker build -t "localhost:5000/addon-operator:example-202" . 36 | docker push localhost:5000/addon-operator:example-202 37 | ``` 38 | 39 | Edit image in addon-operator-deploy.yaml and apply manifests: 40 | 41 | ``` 42 | kubectl create ns example-202 43 | kubectl -n example-202 apply -f deploy/cm.yaml 44 | kubectl -n example-202 apply -f deploy/rbac.yaml 45 | kubectl -n example-202 apply -f deploy/deployment.yaml 46 | ``` 47 | 48 | List Pods to see that 'frontend' and 'backend' are installed: 49 | 50 | ``` 51 | $ kubectl -n example-202 get po 52 | 53 | NAME READY STATUS RESTARTS AGE 54 | addon-operator-5c9df6d4b8-pj9nt 1/1 Running 0 16s 55 | backend-5b764d4464-rdxq4 1/1 Running 0 2m40s 56 | frontend-d8677b8d4-8wkqh 1/1 Running 0 13s 57 | ``` 58 | 59 | See also enabled modules: 60 | 61 | ``` 62 | $ kubectl -n example-202 exec deploy/addon-operator -- /addon-operator module list 63 | 64 | {"enabledModules":["backend","frontend"]} 65 | ``` 66 | 67 | ### Cleanup 68 | 69 | ``` 70 | kubectl delete clusterrolebinding/addon-operator-202 71 | kubectl delete clusterrole/addon-operator-202 72 | kubectl delete ns/example-202 73 | docker rmi localhost:5000/addon-operator:example-202 74 | ``` 75 | -------------------------------------------------------------------------------- /examples/202-module-symlinks/deploy/cm.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: addon-operator 6 | data: {} 7 | -------------------------------------------------------------------------------- /examples/202-module-symlinks/deploy/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: addon-operator 6 | labels: 7 | app: addon-operator 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: addon-operator 13 | strategy: 14 | type: Recreate 15 | template: 16 | metadata: 17 | labels: 18 | app: addon-operator 19 | spec: 20 | containers: 21 | - name: addon-operator 22 | image: localhost:5000/addon-operator:example-202 23 | imagePullPolicy: Always 24 | env: 25 | - name: MODULES_DIR 26 | value: "/module_dir_1:/module_dir_2" 27 | - name: ADDON_OPERATOR_NAMESPACE 28 | valueFrom: 29 | fieldRef: 30 | fieldPath: metadata.namespace 31 | livenessProbe: 32 | httpGet: 33 | path: /healthz 34 | port: 9650 35 | serviceAccountName: addon-operator-202 36 | 37 | -------------------------------------------------------------------------------- /examples/202-module-symlinks/deploy/rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: addon-operator-202 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRole 9 | metadata: 10 | name: addon-operator-202 11 | rules: 12 | - apiGroups: ['*'] 13 | resources: ['*'] 14 | verbs: ['*'] 15 | - nonResourceURLs: ['*'] 16 | verbs: ['*'] 17 | --- 18 | apiVersion: rbac.authorization.k8s.io/v1 19 | kind: ClusterRoleBinding 20 | metadata: 21 | name: addon-operator-202 22 | roleRef: 23 | apiGroup: rbac.authorization.k8s.io 24 | kind: ClusterRole 25 | name: addon-operator-202 26 | subjects: 27 | - kind: ServiceAccount 28 | name: addon-operator-202 29 | namespace: example-202 30 | -------------------------------------------------------------------------------- /examples/202-module-symlinks/global-hooks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/examples/202-module-symlinks/global-hooks/.gitkeep -------------------------------------------------------------------------------- /examples/202-module-symlinks/module_dir_1/001-backend/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: backend 2 | version: 1.0 3 | -------------------------------------------------------------------------------- /examples/202-module-symlinks/module_dir_1/001-backend/hooks/startup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ $1 == "--config" ]] ; then 4 | cat < 0 46 | } 47 | 48 | // HasEqualChecksum returns true if there is a checksum for name equal to the input checksum. 49 | func (c *Checksums) HasEqualChecksum(name string, checksum string) bool { 50 | for chk := range c.sums[name] { 51 | if chk == checksum { 52 | return true 53 | } 54 | } 55 | return false 56 | } 57 | 58 | func (c *Checksums) Names() map[string]struct{} { 59 | names := make(map[string]struct{}) 60 | for name := range c.sums { 61 | names[name] = struct{}{} 62 | } 63 | return names 64 | } 65 | 66 | func (c *Checksums) Dump(moduleName string) map[string]struct{} { 67 | if checksums, has := c.sums[moduleName]; has { 68 | return checksums 69 | } 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /pkg/kube_config_manager/checksums_test.go: -------------------------------------------------------------------------------- 1 | package kube_config_manager 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_Checksums(t *testing.T) { 10 | c := NewChecksums() 11 | 12 | // Adding 13 | c.Add("global", "qwe") 14 | 15 | assert.True(t, c.HasChecksum("global")) 16 | assert.True(t, c.HasEqualChecksum("global", "qwe")) 17 | assert.False(t, c.HasChecksum("non-global"), "Should be false for non added name") 18 | 19 | // Names 20 | expectedNames := map[string]struct{}{ 21 | "global": {}, 22 | } 23 | assert.Equal(t, expectedNames, c.Names()) 24 | 25 | c.Remove("global", "unknown-checksum") 26 | assert.True(t, c.HasEqualChecksum("global", "qwe")) 27 | 28 | c.Remove("global", "qwe") 29 | assert.False(t, c.HasEqualChecksum("global", "qwe")) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/kube_config_manager/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/flant/addon-operator/pkg/utils" 5 | ) 6 | 7 | type KubeConfig struct { 8 | Global *GlobalKubeConfig 9 | Modules map[string]*ModuleKubeConfig 10 | } 11 | 12 | type GlobalKubeConfig struct { 13 | Values utils.Values 14 | Checksum string 15 | } 16 | 17 | // GetValues returns global values, enrich them with top level key 'global' 18 | func (gkc GlobalKubeConfig) GetValues() utils.Values { 19 | if len(gkc.Values) == 0 { 20 | return gkc.Values 21 | } 22 | 23 | if gkc.Values.HasKey("global") { 24 | switch v := gkc.Values["global"].(type) { 25 | case map[string]interface{}: 26 | return utils.Values(v) 27 | 28 | case utils.Values: 29 | return v 30 | } 31 | } 32 | 33 | return gkc.Values 34 | } 35 | 36 | type ModuleKubeConfig struct { 37 | utils.ModuleConfig 38 | Checksum string 39 | } 40 | 41 | func NewConfig() *KubeConfig { 42 | return &KubeConfig{ 43 | Modules: make(map[string]*ModuleKubeConfig), 44 | } 45 | } 46 | 47 | type ( 48 | KubeConfigType string 49 | KubeConfigEvent struct { 50 | Type KubeConfigType 51 | ModuleEnabledStateChanged []string 52 | ModuleValuesChanged []string 53 | GlobalSectionChanged bool 54 | ModuleMaintenanceChanged map[string]utils.Maintenance 55 | } 56 | ) 57 | 58 | const ( 59 | KubeConfigChanged KubeConfigType = "Changed" 60 | KubeConfigInvalid KubeConfigType = "Invalid" 61 | ) 62 | -------------------------------------------------------------------------------- /pkg/kube_config_manager/config/event.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Op string 4 | 5 | const ( 6 | EventDelete Op = "Delete" 7 | EventUpdate Op = "Update" 8 | EventAdd Op = "Add" 9 | ) 10 | 11 | type Event struct { 12 | // Key possible values 13 | // "" - reset the whole config 14 | // "batch" - set global and modules config at once 15 | // "global" - set only global config 16 | // " - set only config for the module 17 | Key string 18 | Config *KubeConfig 19 | Err error 20 | Op Op 21 | } 22 | -------------------------------------------------------------------------------- /pkg/labels.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | const ( 4 | LogKeyBinding = "binding" 5 | LogKeyBindingName = "binding.name" 6 | LogKeyEventType = "event.type" 7 | LogKeyHook = "hook" 8 | LogKeyHookType = "hook.type" 9 | LogKeyModule = "module" 10 | LogKeyQueue = "queue" 11 | LogKeyTaskID = "task.id" 12 | LogKeyTaskFlow = "task.flow" 13 | LogKeyWatchEvent = "watchEvent" 14 | ) 15 | 16 | const ( 17 | MetricKeyActivation = "activation" 18 | MetricKeyBinding = "binding" 19 | MetricKeyHook = "hook" 20 | MetricKeyModule = "module" 21 | MetricKeyQueue = "queue" 22 | ) 23 | -------------------------------------------------------------------------------- /pkg/module_manager/environment_manager/mount.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package environment_manager 4 | 5 | func MountFn(_ string, _ string, _ string, _ uintptr, _ string, _ bool) error { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /pkg/module_manager/environment_manager/mount_linux.go: -------------------------------------------------------------------------------- 1 | package environment_manager 2 | 3 | import "syscall" 4 | 5 | func MountFn(source string, target string, fstype string, flags uintptr, data string, recursiveMount bool) error { 6 | if recursiveMount { 7 | flags = flags | syscall.MS_BIND | syscall.MS_REC 8 | } 9 | 10 | return syscall.Mount(source, target, fstype, flags, data) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/module_manager/go_hook/filter_result.go: -------------------------------------------------------------------------------- 1 | package go_hook 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "reflect" 9 | "strings" 10 | 11 | sdkpkg "github.com/deckhouse/module-sdk/pkg" 12 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 | ) 14 | 15 | type FilterFunc func(*unstructured.Unstructured) (FilterResult, error) 16 | 17 | type FilterResult any 18 | 19 | type Wrapped struct { 20 | Wrapped any 21 | } 22 | 23 | var ( 24 | ErrEmptyWrapped = errors.New("empty filter result") 25 | ErrUnmarshalToTypesNotMatch = errors.New("unmarshal error: input and output types not match") 26 | ) 27 | 28 | func (f *Wrapped) UnmarshalTo(v any) error { 29 | if f.Wrapped == nil { 30 | return ErrEmptyWrapped 31 | } 32 | 33 | rv := reflect.ValueOf(v) 34 | if rv.Kind() != reflect.Pointer || rv.IsNil() { 35 | // error replace with "not pointer" 36 | return fmt.Errorf("reflect.TypeOf(v): %s", reflect.TypeOf(v)) 37 | } 38 | 39 | rw := reflect.ValueOf(f.Wrapped) 40 | if rw.Kind() != reflect.Pointer || rw.IsNil() { 41 | rv.Elem().Set(rw) 42 | 43 | return nil 44 | } 45 | 46 | if rw.Type() != rv.Type() { 47 | return ErrUnmarshalToTypesNotMatch 48 | } 49 | 50 | rv.Elem().Set(rw.Elem()) 51 | 52 | return nil 53 | } 54 | 55 | func (f *Wrapped) String() string { 56 | buf := bytes.NewBuffer([]byte{}) 57 | _ = json.NewEncoder(buf).Encode(f.Wrapped) 58 | 59 | res := buf.String() 60 | 61 | res = strings.TrimSuffix(res, "\n") 62 | 63 | if strings.HasPrefix(res, "\"") { 64 | res = res[1 : len(res)-1] 65 | } 66 | 67 | return res 68 | } 69 | 70 | // type NewSnapshots map[string][]Wrapped 71 | type NewSnapshots map[string][]sdkpkg.Snapshot 72 | 73 | func (s NewSnapshots) Get(name string) []sdkpkg.Snapshot { 74 | return s[name] 75 | } 76 | 77 | type Snapshots map[string][]FilterResult 78 | -------------------------------------------------------------------------------- /pkg/module_manager/go_hook/logger.go: -------------------------------------------------------------------------------- 1 | package go_hook 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/deckhouse/deckhouse/pkg/log" 7 | sdkpkg "github.com/deckhouse/module-sdk/pkg" 8 | ) 9 | 10 | type Logger interface { 11 | sdkpkg.Logger 12 | 13 | // Deprecated: use Debug instead 14 | Debugf(format string, args ...any) 15 | // Deprecated: use Error instead 16 | Errorf(format string, args ...any) 17 | // Deprecated: use Fatal instead 18 | Fatalf(format string, args ...any) 19 | // Deprecated: use Info instead 20 | Infof(format string, args ...any) 21 | // Deprecated: use Log instead 22 | Logf(ctx context.Context, level log.Level, format string, args ...any) 23 | // Deprecated: use Warn instead 24 | Warnf(format string, args ...any) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/module_manager/loader/fs/fs_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/deckhouse/deckhouse/pkg/log" 9 | . "github.com/onsi/gomega" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestNewFileSystemLoader(t *testing.T) { 15 | tmpDir, _ := os.MkdirTemp("", "") 16 | defer os.RemoveAll(tmpDir) 17 | 18 | _ = os.MkdirAll(filepath.Join(tmpDir, "modules", "001-foo-bar"), 0o777) 19 | 20 | err := os.WriteFile(filepath.Join(tmpDir, "modules", "values.yaml"), []byte(` 21 | fooBarEnabled: true 22 | fooBar: 23 | replicas: 2 24 | hello: 25 | world: "bzzzz" 26 | `), 0o666) 27 | require.NoError(t, err) 28 | 29 | err = os.WriteFile(filepath.Join(tmpDir, "modules", "001-foo-bar", "values.yaml"), []byte(` 30 | fooBar: 31 | replicas: 3 32 | hello: 33 | world: "xxx" 34 | `), 0o666) 35 | require.NoError(t, err) 36 | 37 | _ = os.MkdirAll(filepath.Join(tmpDir, "modules", "001-foo-bar", "openapi"), 0o777) 38 | 39 | err = os.WriteFile(filepath.Join(tmpDir, "modules", "001-foo-bar", "values.yaml"), []byte(` 40 | fooBar: 41 | replicas: 3 42 | hello: 43 | world: "xxx" 44 | `), 0o666) 45 | require.NoError(t, err) 46 | 47 | loader := NewFileSystemLoader(filepath.Join(tmpDir, "modules"), log.NewNop()) 48 | modules, err := loader.LoadModules() 49 | require.NoError(t, err) 50 | m := modules[0] 51 | assert.Equal(t, "foo-bar", m.GetName()) 52 | assert.YAMLEq(t, ` 53 | hello: 54 | world: xxx 55 | replicas: 3 56 | `, m.GetValues(false).AsString("yaml")) 57 | } 58 | 59 | func TestDirWithSymlinks(t *testing.T) { 60 | g := NewWithT(t) 61 | dir := "testdata/module_loader/dir1" 62 | 63 | ld := NewFileSystemLoader(dir, log.NewNop()) 64 | 65 | mods, err := ld.LoadModules() 66 | g.Expect(err).ShouldNot(HaveOccurred()) 67 | 68 | g.Expect(mods).Should(HaveLen(2)) 69 | } 70 | 71 | func TestLoadMultiDir(t *testing.T) { 72 | g := NewWithT(t) 73 | dirs := "testdata/module_loader/dir2:testdata/module_loader/dir3" 74 | 75 | ld := NewFileSystemLoader(dirs, log.NewNop()) 76 | 77 | mods, err := ld.LoadModules() 78 | g.Expect(err).ShouldNot(HaveOccurred()) 79 | 80 | g.Expect(mods).Should(HaveLen(2)) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/module_manager/loader/fs/testdata/module_loader/dir1/001-module-one: -------------------------------------------------------------------------------- 1 | ../modules/001-module-one -------------------------------------------------------------------------------- /pkg/module_manager/loader/fs/testdata/module_loader/dir1/002-module-two: -------------------------------------------------------------------------------- 1 | ../modules/002-module-two -------------------------------------------------------------------------------- /pkg/module_manager/loader/fs/testdata/module_loader/dir1/values.yaml: -------------------------------------------------------------------------------- 1 | moduleOne: 2 | param1: val1 3 | param2: val2 4 | moduleTwo: 5 | param1: val1 6 | -------------------------------------------------------------------------------- /pkg/module_manager/loader/fs/testdata/module_loader/dir2/012-mod-two: -------------------------------------------------------------------------------- 1 | ../modules/002-module-two -------------------------------------------------------------------------------- /pkg/module_manager/loader/fs/testdata/module_loader/dir2/100-mod-one: -------------------------------------------------------------------------------- 1 | ../modules/001-module-one -------------------------------------------------------------------------------- /pkg/module_manager/loader/fs/testdata/module_loader/dir2/values.yaml: -------------------------------------------------------------------------------- 1 | modOne: 2 | param1: val2 3 | param2: val2 4 | 5 | modTwo: 6 | param1: val2 7 | param2: val2 8 | -------------------------------------------------------------------------------- /pkg/module_manager/loader/fs/testdata/module_loader/dir3/values.yaml: -------------------------------------------------------------------------------- 1 | modOne: 2 | param1: val3 3 | param2: val3 4 | modTwo: 5 | param1: val3 6 | param2: val3 7 | moduleThree: 8 | param1: val3 9 | -------------------------------------------------------------------------------- /pkg/module_manager/loader/fs/testdata/module_loader/modules/001-module-one/.gitkeep: -------------------------------------------------------------------------------- 1 | global: 2 | a: 1 3 | b: 2 4 | c: 3 5 | d: ["a", "b", "c"] 6 | -------------------------------------------------------------------------------- /pkg/module_manager/loader/fs/testdata/module_loader/modules/002-module-two/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/loader/fs/testdata/module_loader/modules/002-module-two/.gitkeep -------------------------------------------------------------------------------- /pkg/module_manager/loader/fs/testdata/module_loader/modules/003-module-three/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/loader/fs/testdata/module_loader/modules/003-module-three/.gitkeep -------------------------------------------------------------------------------- /pkg/module_manager/loader/loader.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "github.com/flant/addon-operator/pkg/module_manager/models/modules" 5 | ) 6 | 7 | type ModuleLoader interface { 8 | LoadModules() ([]*modules.BasicModule, error) 9 | LoadModule(moduleSource string, modulePath string) (*modules.BasicModule, error) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/module_manager/models/hooks/dependency.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "context" 5 | 6 | sdkpkg "github.com/deckhouse/module-sdk/pkg" 7 | 8 | environmentmanager "github.com/flant/addon-operator/pkg/module_manager/environment_manager" 9 | gohook "github.com/flant/addon-operator/pkg/module_manager/go_hook" 10 | "github.com/flant/addon-operator/pkg/module_manager/models/hooks/kind" 11 | "github.com/flant/addon-operator/pkg/utils" 12 | bindingcontext "github.com/flant/shell-operator/pkg/hook/binding_context" 13 | "github.com/flant/shell-operator/pkg/hook/config" 14 | "github.com/flant/shell-operator/pkg/hook/controller" 15 | metricoperation "github.com/flant/shell-operator/pkg/metric_storage/operation" 16 | ) 17 | 18 | type hooksMetricsStorage interface { 19 | SendBatch([]metricoperation.MetricOperation, map[string]string) error 20 | } 21 | 22 | type kubeConfigManager interface { 23 | SaveConfigValues(moduleName string, configValuesPatch utils.Values) error 24 | } 25 | 26 | type metricStorage interface { 27 | HistogramObserve(metric string, value float64, labels map[string]string, buckets []float64) 28 | GaugeSet(metric string, value float64, labels map[string]string) 29 | } 30 | 31 | type kubeObjectPatcher interface { 32 | ExecuteOperations([]sdkpkg.PatchCollectorOperation) error 33 | } 34 | 35 | type globalValuesGetter interface { 36 | GetValues(bool) utils.Values 37 | GetConfigValues(bool) utils.Values 38 | } 39 | 40 | // HookExecutionDependencyContainer container for all hook execution dependencies 41 | type HookExecutionDependencyContainer struct { 42 | HookMetricsStorage hooksMetricsStorage 43 | KubeConfigManager kubeConfigManager 44 | KubeObjectPatcher kubeObjectPatcher 45 | MetricStorage metricStorage 46 | GlobalValuesGetter globalValuesGetter 47 | EnvironmentManager *environmentmanager.Manager 48 | } 49 | 50 | type executableHook interface { 51 | GetName() string 52 | GetPath() string 53 | 54 | Execute(ctx context.Context, configVersion string, bContext []bindingcontext.BindingContext, moduleSafeName string, configValues, values utils.Values, logLabels map[string]string) (result *kind.HookResult, err error) 55 | RateLimitWait(ctx context.Context) error 56 | 57 | WithHookController(ctrl *controller.HookController) 58 | GetHookController() *controller.HookController 59 | WithTmpDir(tmpDir string) 60 | 61 | GetKind() kind.HookKind 62 | 63 | BackportHookConfig(cfg *config.HookConfig) 64 | GetHookConfigDescription() string 65 | } 66 | 67 | type hookConfigLoader gohook.HookConfigLoader 68 | -------------------------------------------------------------------------------- /pkg/module_manager/models/hooks/global_hook_test.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | // 4 | // import ( 5 | // "github.com/flant/addon-operator/pkg/module_manager/models/hooks" 6 | // "testing" 7 | // 8 | // . "github.com/onsi/gomega" 9 | // 10 | // . "github.com/flant/addon-operator/pkg/hook/types" 11 | // . "github.com/flant/shell-operator/pkg/hook/types" 12 | //) 13 | // 14 | // func Test_GlobalHook_WithConfig(t *testing.T) { 15 | // g := NewWithT(t) 16 | // 17 | // var gh *hooks.GlobalHook 18 | // var err error 19 | // 20 | // tests := []struct { 21 | // name string 22 | // config string 23 | // assertFn func() 24 | // }{ 25 | // { 26 | // "asd", 27 | // `configVersion: v1 28 | //onStartup: 10 29 | //kubernetes: 30 | //- name: pods 31 | // kind: Pod 32 | //schedule: 33 | //- name: planned 34 | // crontab: '* * * * *' 35 | //afterAll: 22 36 | //beforeAll: 23 37 | // `, 38 | // func() { 39 | // g.Expect(err).ShouldNot(HaveOccurred()) 40 | // g.Expect(gh).ShouldNot(BeNil()) 41 | // config := gh.Config 42 | // g.Expect(gh.Order(OnStartup)).To(Equal(10.0)) 43 | // g.Expect(gh.Order(BeforeAll)).To(Equal(23.0)) 44 | // g.Expect(gh.Order(AfterAll)).To(Equal(22.0)) 45 | // g.Expect(config.OnKubernetesEvents).To(HaveLen(1)) 46 | // g.Expect(config.Schedules).To(HaveLen(1)) 47 | // 48 | // g.Expect(gh.GetConfigDescription()).To(ContainSubstring("beforeAll:23, afterAll:22, OnStartup:10")) 49 | // }, 50 | // }, 51 | // } 52 | // 53 | // for _, tt := range tests { 54 | // t.Run(tt.name, func(t *testing.T) { 55 | // gh = NewGlobalHook("test", "/global-hooks/test.sh") 56 | // err = gh.WithConfig([]byte(tt.config)) 57 | // tt.assertFn() 58 | // }) 59 | // } 60 | //} 61 | -------------------------------------------------------------------------------- /pkg/module_manager/models/hooks/kind/gohook_test.go: -------------------------------------------------------------------------------- 1 | package kind 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | . "github.com/onsi/gomega" 8 | 9 | gohook "github.com/flant/addon-operator/pkg/module_manager/go_hook" 10 | . "github.com/flant/shell-operator/pkg/hook/binding_context" 11 | ) 12 | 13 | func Test_Config_GoHook(t *testing.T) { 14 | g := NewWithT(t) 15 | 16 | gh := NewGoHook(&gohook.HookConfig{ 17 | OnAfterAll: &gohook.OrderedConfig{Order: 5}, 18 | }, func(input *gohook.HookInput) error { 19 | input.Values.Set("test", "test") 20 | input.MetricsCollector.Set("test", 1.0, nil) 21 | 22 | return nil 23 | }) 24 | 25 | bc := make([]BindingContext, 0) 26 | 27 | res, err := gh.Execute(context.Background(), "", bc, "", nil, nil, nil) 28 | g.Expect(err).ShouldNot(HaveOccurred()) 29 | g.Expect(res.Patches).ShouldNot(BeEmpty()) 30 | g.Expect(res.Metrics).ShouldNot(BeEmpty()) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/module_manager/models/hooks/kind/kind.go: -------------------------------------------------------------------------------- 1 | package kind 2 | 3 | import ( 4 | sdkpkg "github.com/deckhouse/module-sdk/pkg" 5 | 6 | gohook "github.com/flant/addon-operator/pkg/module_manager/go_hook" 7 | "github.com/flant/addon-operator/pkg/utils" 8 | "github.com/flant/shell-operator/pkg/executor" 9 | metricoperation "github.com/flant/shell-operator/pkg/metric_storage/operation" 10 | ) 11 | 12 | // HookKind kind of the hook 13 | type HookKind string 14 | 15 | var ( 16 | // HookKindGo for go hooks 17 | HookKindGo HookKind = "go" 18 | // HookKindShell for shell hooks (bash, python, etc) 19 | HookKindShell HookKind = "shell" 20 | ) 21 | 22 | // HookResult returns result of a hook execution 23 | type HookResult struct { 24 | Usage *executor.CmdUsage 25 | Patches map[utils.ValuesPatchType]*utils.ValuesPatch 26 | Metrics []metricoperation.MetricOperation 27 | ObjectPatcherOperations []sdkpkg.PatchCollectorOperation 28 | BindingActions []gohook.BindingAction 29 | } 30 | -------------------------------------------------------------------------------- /pkg/module_manager/models/modules/basic_test.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "testing" 7 | 8 | sdkutils "github.com/deckhouse/module-sdk/pkg/utils" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/flant/addon-operator/pkg/utils" 13 | ) 14 | 15 | func TestHandleModulePatch(t *testing.T) { 16 | valuesStr := ` 17 | foo: 18 | bar: baz 19 | ` 20 | value, err := utils.NewValuesFromBytes([]byte(valuesStr)) 21 | require.NoError(t, err) 22 | bm, err := NewBasicModule("test-1", "/tmp/test", 100, value, nil, nil) 23 | require.NoError(t, err) 24 | 25 | patch := utils.ValuesPatch{Operations: []*sdkutils.ValuesPatchOperation{ 26 | { 27 | Op: "add", 28 | Path: "/test1/foo/xxx", 29 | Value: json.RawMessage(`"yyy"`), 30 | }, 31 | { 32 | Op: "remove", 33 | Path: "/test1/foo/bar", 34 | Value: json.RawMessage(`"zxc"`), 35 | }, 36 | }} 37 | res, err := bm.handleModuleValuesPatch(bm.GetValues(true), patch) 38 | require.NoError(t, err) 39 | assert.True(t, res.ValuesChanged) 40 | assert.YAMLEq(t, ` 41 | foo: 42 | xxx: yyy 43 | `, 44 | res.Values.AsString("yaml")) 45 | } 46 | 47 | func TestIsFileBatchHook(t *testing.T) { 48 | hookPath := "./testdata/batchhook" 49 | 50 | err := os.WriteFile(hookPath, []byte(`#!/bin/bash 51 | if [ "${1}" == "hook" ] && [ "${2}" == "list" ]; then 52 | echo "Found 3 items" 53 | fi 54 | `), 0o555) 55 | require.NoError(t, err) 56 | 57 | defer os.Remove(hookPath) 58 | 59 | fileInfo, err := os.Stat(hookPath) 60 | require.NoError(t, err) 61 | 62 | err = IsFileBatchHook("moduleName", hookPath, fileInfo) 63 | require.NoError(t, err) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/module_manager/models/modules/events/events.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // ModuleEventType type of the event 4 | type ModuleEventType int 5 | 6 | const ( 7 | ModuleRegistered ModuleEventType = iota 8 | ModuleEnabled 9 | ModuleDisabled 10 | ModuleStateChanged 11 | ModuleConfigChanged 12 | 13 | FirstConvergeDone 14 | ) 15 | 16 | // ModuleEvent event model for hooks 17 | type ModuleEvent struct { 18 | ModuleName string 19 | EventType ModuleEventType 20 | } 21 | -------------------------------------------------------------------------------- /pkg/module_manager/models/modules/hook_storage.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | 7 | "github.com/flant/addon-operator/pkg/module_manager/models/hooks" 8 | sh_op_types "github.com/flant/shell-operator/pkg/hook/types" 9 | ) 10 | 11 | // HooksStorage keep module hooks in order 12 | type HooksStorage struct { 13 | registered bool 14 | controllersReady bool 15 | lock sync.RWMutex 16 | byBinding map[sh_op_types.BindingType][]*hooks.ModuleHook 17 | byName map[string]*hooks.ModuleHook 18 | } 19 | 20 | func newHooksStorage() *HooksStorage { 21 | return &HooksStorage{ 22 | registered: false, 23 | byBinding: make(map[sh_op_types.BindingType][]*hooks.ModuleHook), 24 | byName: make(map[string]*hooks.ModuleHook), 25 | } 26 | } 27 | 28 | func (hs *HooksStorage) AddHook(hk *hooks.ModuleHook) { 29 | hs.lock.Lock() 30 | defer hs.lock.Unlock() 31 | 32 | hName := hk.GetName() 33 | hs.byName[hName] = hk 34 | for _, binding := range hk.GetHookConfig().Bindings() { 35 | hs.byBinding[binding] = append(hs.byBinding[binding], hk) 36 | } 37 | } 38 | 39 | func (hs *HooksStorage) getHooks(bt ...sh_op_types.BindingType) []*hooks.ModuleHook { 40 | hs.lock.RLock() 41 | defer hs.lock.RUnlock() 42 | 43 | if len(bt) > 0 { 44 | t := bt[0] 45 | res, ok := hs.byBinding[t] 46 | if !ok { 47 | return []*hooks.ModuleHook{} 48 | } 49 | sort.Slice(res, func(i, j int) bool { 50 | return res[i].Order(t) < res[j].Order(t) 51 | }) 52 | 53 | return res 54 | } 55 | 56 | // return all hooks 57 | res := make([]*hooks.ModuleHook, 0, len(hs.byName)) 58 | for _, h := range hs.byName { 59 | res = append(res, h) 60 | } 61 | 62 | sort.Slice(res, func(i, j int) bool { 63 | return res[i].GetName() < res[j].GetName() 64 | }) 65 | 66 | return res 67 | } 68 | 69 | func (hs *HooksStorage) getHookByName(name string) *hooks.ModuleHook { 70 | hs.lock.RLock() 71 | defer hs.lock.RUnlock() 72 | 73 | return hs.byName[name] 74 | } 75 | 76 | func (hs *HooksStorage) clean() { 77 | hs.lock.Lock() 78 | defer hs.lock.Unlock() 79 | 80 | hs.byBinding = make(map[sh_op_types.BindingType][]*hooks.ModuleHook) 81 | hs.byName = make(map[string]*hooks.ModuleHook) 82 | hs.registered = false 83 | hs.controllersReady = false 84 | } 85 | -------------------------------------------------------------------------------- /pkg/module_manager/models/modules/module_options.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import "github.com/deckhouse/deckhouse/pkg/log" 4 | 5 | type Option func(optionsApplier ModuleOptionApplier) 6 | 7 | func (opt Option) Apply(o ModuleOptionApplier) { 8 | opt(o) 9 | } 10 | 11 | func WithLogger(logger *log.Logger) Option { 12 | return func(optionsApplier ModuleOptionApplier) { 13 | optionsApplier.WithLogger(logger) 14 | } 15 | } 16 | 17 | type ModuleOption interface { 18 | Apply(optsApplier ModuleOptionApplier) 19 | } 20 | 21 | type ModuleOptionApplier interface { 22 | WithLogger(logger *log.Logger) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/module_manager/models/modules/values_defaulting_transformers.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "github.com/go-openapi/spec" 5 | 6 | "github.com/flant/addon-operator/pkg/utils" 7 | "github.com/flant/addon-operator/pkg/values/validation" 8 | ) 9 | 10 | type transformer interface { 11 | Transform(values utils.Values) utils.Values 12 | } 13 | 14 | type applyDefaults struct { 15 | SchemaType validation.SchemaType 16 | Schemas map[validation.SchemaType]*spec.Schema 17 | } 18 | 19 | func (a *applyDefaults) Transform(values utils.Values) utils.Values { 20 | if a.Schemas == nil { 21 | return values 22 | } 23 | 24 | s := a.Schemas[a.SchemaType] 25 | if s == nil { 26 | return values 27 | } 28 | 29 | res := values.Copy() 30 | validation.ApplyDefaults(res, s) 31 | 32 | return res 33 | } 34 | -------------------------------------------------------------------------------- /pkg/module_manager/models/modules/values_layered.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import "github.com/flant/addon-operator/pkg/utils" 4 | 5 | type valuesTransform func(values utils.Values) utils.Values 6 | 7 | type valuesTransformer interface { 8 | Transform(values utils.Values) utils.Values 9 | } 10 | 11 | func mergeLayers(initial utils.Values, layers ...interface{}) utils.Values { 12 | res := utils.MergeValues(initial) 13 | 14 | for _, layer := range layers { 15 | switch layer := layer.(type) { 16 | case utils.Values: 17 | res = utils.MergeValues(res, layer) 18 | case map[string]interface{}: 19 | res = utils.MergeValues(res, layer) 20 | case string: 21 | // Ignore error to be handy for tests. 22 | tmp, _ := utils.NewValuesFromBytes([]byte(layer)) 23 | res = utils.MergeValues(res, tmp) 24 | case valuesTransform: 25 | res = utils.MergeValues(res, layer(res)) 26 | case valuesTransformer: 27 | res = utils.MergeValues(res, layer.Transform(res)) 28 | case nil: 29 | continue 30 | } 31 | } 32 | 33 | return res 34 | } 35 | -------------------------------------------------------------------------------- /pkg/module_manager/models/modules/values_layered_test.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/flant/addon-operator/pkg/utils" 9 | ) 10 | 11 | func TestMergeLayers(t *testing.T) { 12 | globalValues := utils.Values{ 13 | "global": map[string]interface{}{ 14 | "enabledModules": []string{"module1", "module2"}, 15 | "highAvailability": true, 16 | }, 17 | } 18 | res := mergeLayers( 19 | utils.Values{}, 20 | globalValues, 21 | utils.Values{ 22 | "global": map[string]interface{}{ 23 | "enabledModules": []string{"module3"}, 24 | "logLevel": "Info", 25 | }, 26 | }, 27 | ) 28 | assert.YAMLEq(t, ` 29 | global: 30 | logLevel: "Info" 31 | highAvailability: true 32 | enabledModules: 33 | - module3 34 | `, res.AsString("yaml")) 35 | 36 | assert.YAMLEq(t, ` 37 | global: 38 | highAvailability: true 39 | enabledModules: 40 | - module1 41 | - module2 42 | `, globalValues.AsString("yaml")) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/module_manager/models/moduleset/moduleset_test.go: -------------------------------------------------------------------------------- 1 | package moduleset 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/gomega" 7 | 8 | "github.com/flant/addon-operator/pkg/module_manager/models/modules" 9 | ) 10 | 11 | func TestBasicModuleSet(t *testing.T) { 12 | g := NewWithT(t) 13 | ms := new(ModulesSet) 14 | 15 | ms.Add(&modules.BasicModule{ 16 | Name: "BasicModule-two", 17 | Order: 10, 18 | }) 19 | ms.Add(&modules.BasicModule{ 20 | Name: "BasicModule-one", 21 | Order: 5, 22 | }) 23 | ms.Add(&modules.BasicModule{ 24 | Name: "BasicModule-three-two", 25 | Order: 15, 26 | }) 27 | ms.Add(&modules.BasicModule{ 28 | Name: "BasicModule-four", 29 | Order: 1, 30 | }) 31 | ms.Add(&modules.BasicModule{ 32 | Name: "BasicModule-three-one", 33 | Order: 15, 34 | }) 35 | // "overridden" BasicModule 36 | ms.Add(&modules.BasicModule{ 37 | Name: "BasicModule-four", 38 | Order: 20, 39 | }) 40 | ms.SetInited() 41 | 42 | expectNames := []string{ 43 | "BasicModule-one", 44 | "BasicModule-two", 45 | "BasicModule-three-one", 46 | "BasicModule-three-two", 47 | "BasicModule-four", 48 | } 49 | 50 | g.Expect(ms.NamesInOrder()).Should(Equal(expectNames)) 51 | g.Expect(ms.Has("BasicModule-four")).Should(BeTrue(), "should have BasicModule-four") 52 | g.Expect(ms.Get("BasicModule-four").Order).Should(Equal(uint32(20)), "should have BasicModule-four with order:20") 53 | g.Expect(ms.IsInited()).Should(BeTrue(), "should be inited") 54 | } 55 | -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/extenders/dynamically_enabled/dynamic.go: -------------------------------------------------------------------------------- 1 | package dynamically_enabled 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "sync" 7 | 8 | "github.com/deckhouse/deckhouse/pkg/log" 9 | 10 | "github.com/flant/addon-operator/pkg/module_manager/scheduler/extenders" 11 | ) 12 | 13 | const ( 14 | Name extenders.ExtenderName = "DynamicallyEnabled" 15 | ) 16 | 17 | type Extender struct { 18 | notifyCh chan extenders.ExtenderEvent 19 | l sync.RWMutex 20 | modulesStatus map[string]bool 21 | } 22 | 23 | type DynamicExtenderEvent struct{} 24 | 25 | func NewExtender() *Extender { 26 | e := &Extender{ 27 | modulesStatus: make(map[string]bool), 28 | } 29 | return e 30 | } 31 | 32 | func (e *Extender) UpdateStatus(moduleName, operation string, value bool) { 33 | e.l.Lock() 34 | switch operation { 35 | case "add": 36 | status, found := e.modulesStatus[moduleName] 37 | if !found || (found && status != value) { 38 | e.modulesStatus[moduleName] = value 39 | e.sendNotify() 40 | } 41 | case "remove": 42 | if _, found := e.modulesStatus[moduleName]; found { 43 | delete(e.modulesStatus, moduleName) 44 | e.sendNotify() 45 | } 46 | default: 47 | log.Warn("Unknown patch operation", 48 | slog.String("operation", operation)) 49 | } 50 | e.l.Unlock() 51 | } 52 | 53 | func (e *Extender) sendNotify() { 54 | if e.notifyCh != nil { 55 | e.notifyCh <- extenders.ExtenderEvent{ 56 | ExtenderName: Name, 57 | EncapsulatedEvent: DynamicExtenderEvent{}, 58 | } 59 | } 60 | } 61 | 62 | func (e *Extender) Name() extenders.ExtenderName { 63 | return Name 64 | } 65 | 66 | func (e *Extender) Filter(moduleName string, _ map[string]string) (*bool, error) { 67 | e.l.RLock() 68 | defer e.l.RUnlock() 69 | 70 | if val, found := e.modulesStatus[moduleName]; found { 71 | return &val, nil 72 | } 73 | 74 | return nil, nil 75 | } 76 | 77 | func (e *Extender) IsTerminator() bool { 78 | return false 79 | } 80 | 81 | func (e *Extender) SetNotifyChannel(_ context.Context, ch chan extenders.ExtenderEvent) { 82 | e.notifyCh = ch 83 | } 84 | -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/extenders/dynamically_enabled/dynamic_test.go: -------------------------------------------------------------------------------- 1 | package dynamically_enabled 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/flant/addon-operator/pkg/module_manager/scheduler/extenders" 10 | ) 11 | 12 | func TestUpdateStatus(t *testing.T) { 13 | de := NewExtender() 14 | ch := make(chan extenders.ExtenderEvent) 15 | de.SetNotifyChannel(context.TODO(), ch) 16 | var boolNilP *bool 17 | 18 | go func() { 19 | //nolint:revive 20 | for range ch { 21 | } 22 | }() 23 | 24 | de.UpdateStatus("l2-load-balancer", "add", true) 25 | de.UpdateStatus("node-local-dns", "remove", true) 26 | de.UpdateStatus("openstack-cloud-provider", "add", true) 27 | de.UpdateStatus("openstack-cloud-provider", "add", false) 28 | logLabels := map[string]string{} 29 | 30 | filterResult, _ := de.Filter("l2-load-balancer", logLabels) 31 | assert.Equal(t, true, *filterResult) 32 | filterResult, _ = de.Filter("node-local-dns", logLabels) 33 | assert.Equal(t, boolNilP, filterResult) 34 | filterResult, _ = de.Filter("openstack-cloud-provider", logLabels) 35 | assert.Equal(t, false, *filterResult) 36 | 37 | de.UpdateStatus("node-local-dns", "add", false) 38 | de.UpdateStatus("openstack-cloud-provider", "add", true) 39 | 40 | filterResult, _ = de.Filter("l2-load-balancer", logLabels) 41 | assert.Equal(t, true, *filterResult) 42 | filterResult, _ = de.Filter("node-local-dns", logLabels) 43 | assert.Equal(t, false, *filterResult) 44 | filterResult, _ = de.Filter("openstack-cloud-provider", logLabels) 45 | assert.Equal(t, true, *filterResult) 46 | 47 | de.UpdateStatus("l2-load-balancer", "remove", true) 48 | 49 | filterResult, _ = de.Filter("l2-load-balancer", logLabels) 50 | assert.Equal(t, boolNilP, filterResult) 51 | filterResult, _ = de.Filter("node-local-dns", logLabels) 52 | assert.Equal(t, false, *filterResult) 53 | filterResult, _ = de.Filter("openstack-cloud-provider", logLabels) 54 | assert.Equal(t, true, *filterResult) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/extenders/error/permanent.go: -------------------------------------------------------------------------------- 1 | package error 2 | 3 | // PermanentError signals that the operation should stop the module manager immediately 4 | type PermanentError struct { 5 | Err error 6 | } 7 | 8 | func (e *PermanentError) Error() string { 9 | return e.Err.Error() 10 | } 11 | 12 | func (e *PermanentError) Unwrap() error { 13 | return e.Err 14 | } 15 | 16 | // Permanent wraps the given err in a *PermanentError. 17 | func Permanent(err error) *PermanentError { 18 | if err == nil { 19 | return nil 20 | } 21 | return &PermanentError{ 22 | Err: err, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/extenders/extenders.go: -------------------------------------------------------------------------------- 1 | package extenders 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type ExtenderEvent struct { 8 | ExtenderName ExtenderName 9 | EncapsulatedEvent interface{} 10 | } 11 | 12 | type ExtenderName string 13 | 14 | type Extender interface { 15 | // Name returns the extender's name 16 | Name() ExtenderName 17 | // Filter returns the result of applying the extender 18 | Filter(moduleName string, logLabels map[string]string) (*bool, error) 19 | // IsTerminator marks extender that can only disable an enabled module if some requirement isn't met. 20 | // By design, terminators can't be overridden by other extenders. 21 | IsTerminator() bool 22 | } 23 | 24 | type NotificationExtender interface { 25 | // SetNotifyChannel sets output channel for an extender's events, to notify when module state could be changed during the runtime 26 | SetNotifyChannel(context.Context, chan ExtenderEvent) 27 | } 28 | 29 | type TopologicalExtender interface { 30 | // GetTopologicalHints returns the list of vertices that should be connected to the specified vertex 31 | GetTopologicalHints(string) []string 32 | } 33 | 34 | // Hail to enabled scripts 35 | type StatefulExtender interface { 36 | // SetModulesStateHelper sets a helper function to get the list of enabled modules according to the latest vertex state buffer 37 | SetModulesStateHelper(func() []string) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/extenders/kube_config/kube_config.go: -------------------------------------------------------------------------------- 1 | package kube_config 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/flant/addon-operator/pkg/kube_config_manager/config" 7 | "github.com/flant/addon-operator/pkg/module_manager/scheduler/extenders" 8 | ) 9 | 10 | const ( 11 | Name extenders.ExtenderName = "KubeConfig" 12 | ) 13 | 14 | type kubeConfigManager interface { 15 | IsModuleEnabled(moduleName string) *bool 16 | KubeConfigEventCh() chan config.KubeConfigEvent 17 | } 18 | 19 | type Extender struct { 20 | notifyCh chan extenders.ExtenderEvent 21 | kubeConfigManager kubeConfigManager 22 | } 23 | 24 | func NewExtender(kcm kubeConfigManager) *Extender { 25 | e := &Extender{ 26 | kubeConfigManager: kcm, 27 | } 28 | 29 | return e 30 | } 31 | 32 | func (e *Extender) Name() extenders.ExtenderName { 33 | return Name 34 | } 35 | 36 | func (e *Extender) Filter(moduleName string, _ map[string]string) (*bool, error) { 37 | return e.kubeConfigManager.IsModuleEnabled(moduleName), nil 38 | } 39 | 40 | func (e *Extender) IsTerminator() bool { 41 | return false 42 | } 43 | 44 | func (e *Extender) sendNotify(kubeConfigEvent config.KubeConfigEvent) { 45 | if e.notifyCh != nil { 46 | e.notifyCh <- extenders.ExtenderEvent{ 47 | ExtenderName: Name, 48 | EncapsulatedEvent: kubeConfigEvent, 49 | } 50 | } 51 | } 52 | 53 | func (e *Extender) SetNotifyChannel(ctx context.Context, ch chan extenders.ExtenderEvent) { 54 | e.notifyCh = ch 55 | go func() { 56 | for { 57 | select { 58 | case kubeConfigEvent := <-e.kubeConfigManager.KubeConfigEventCh(): 59 | e.sendNotify(kubeConfigEvent) 60 | case <-ctx.Done(): 61 | return 62 | } 63 | } 64 | }() 65 | } 66 | -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/extenders/script_enabled/testdata/015-admission-policy-engine/enabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/scheduler/extenders/script_enabled/testdata/015-admission-policy-engine/enabled -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/extenders/script_enabled/testdata/020-node-local-dns/enabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/scheduler/extenders/script_enabled/testdata/020-node-local-dns/enabled -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/extenders/script_enabled/testdata/031-foo-bar/enabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/scheduler/extenders/script_enabled/testdata/031-foo-bar/enabled -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/extenders/script_enabled/testdata/045-chrony/enabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/scheduler/extenders/script_enabled/testdata/045-chrony/enabled -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/extenders/script_enabled/testdata/402-ingress-nginx/enabled: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exit 0 4 | -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/extenders/static/static.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/deckhouse/deckhouse/pkg/log" 10 | "github.com/ettle/strcase" 11 | "gopkg.in/yaml.v3" 12 | 13 | "github.com/flant/addon-operator/pkg/module_manager/scheduler/extenders" 14 | "github.com/flant/addon-operator/pkg/utils" 15 | ) 16 | 17 | const ( 18 | Name extenders.ExtenderName = "Static" 19 | ) 20 | 21 | type Extender struct { 22 | modulesStatus map[string]bool 23 | } 24 | 25 | func NewExtender(staticValuesFilePaths string) (*Extender, error) { 26 | result := make(map[string]bool) 27 | dirs := utils.SplitToPaths(staticValuesFilePaths) 28 | for _, dir := range dirs { 29 | valuesFile := filepath.Join(dir, "values.yaml") 30 | fileInfo, err := os.Stat(valuesFile) 31 | if err != nil { 32 | log.Warn("Couldn't stat file", 33 | slog.String("file", valuesFile)) 34 | continue 35 | } 36 | 37 | if fileInfo.IsDir() { 38 | log.Error("File is a directory", 39 | slog.String("file", valuesFile)) 40 | continue 41 | } 42 | 43 | f, err := os.Open(valuesFile) 44 | if err != nil { 45 | if os.IsNotExist(err) { 46 | log.Debug("File doesn't exist", 47 | slog.String("file", valuesFile)) 48 | continue 49 | } 50 | return nil, err 51 | } 52 | defer f.Close() 53 | 54 | m := make(map[string]interface{}) 55 | 56 | err = yaml.NewDecoder(f).Decode(&m) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | for k, v := range m { 62 | if strings.HasSuffix(k, utils.EnabledSuffix) { 63 | m := strings.TrimSuffix(k, utils.EnabledSuffix) 64 | keb := strcase.ToKebab(m) 65 | result[keb] = v.(bool) 66 | } 67 | } 68 | } 69 | 70 | e := &Extender{ 71 | modulesStatus: result, 72 | } 73 | 74 | return e, nil 75 | } 76 | 77 | func (e *Extender) Name() extenders.ExtenderName { 78 | return Name 79 | } 80 | 81 | func (e *Extender) Filter(moduleName string, _ map[string]string) (*bool, error) { 82 | if val, found := e.modulesStatus[moduleName]; found { 83 | return &val, nil 84 | } 85 | 86 | return nil, nil 87 | } 88 | 89 | func (e *Extender) IsTerminator() bool { 90 | return false 91 | } 92 | -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/node/mock/node_mock.go: -------------------------------------------------------------------------------- 1 | package node_mock 2 | 3 | import ( 4 | "context" 5 | "slices" 6 | ) 7 | 8 | type MockModule struct { 9 | EnabledScriptResult bool 10 | EnabledScriptErr error 11 | EnabledModules *[]string 12 | ListOfRequiredModules []string 13 | Name string 14 | Path string 15 | Order uint32 16 | } 17 | 18 | func (m MockModule) GetName() string { 19 | return m.Name 20 | } 21 | 22 | func (m MockModule) GetPath() string { 23 | return m.Path 24 | } 25 | 26 | func (m MockModule) GetOrder() uint32 { 27 | return m.Order 28 | } 29 | 30 | func (m MockModule) RunEnabledScript(_ context.Context, _ string, _ []string, _ map[string]string) (bool, error) { 31 | if m.EnabledScriptErr != nil { 32 | return false, m.EnabledScriptErr 33 | } 34 | 35 | depsEnabled := true 36 | if len(m.ListOfRequiredModules) > 0 && m.EnabledModules != nil { 37 | for _, requiredModule := range m.ListOfRequiredModules { 38 | if !slices.Contains(*m.EnabledModules, requiredModule) { 39 | depsEnabled = false 40 | break 41 | } 42 | } 43 | } 44 | 45 | if depsEnabled && m.EnabledScriptResult && m.EnabledModules != nil { 46 | *m.EnabledModules = append(*m.EnabledModules, m.Name) 47 | } 48 | 49 | return depsEnabled && m.EnabledScriptResult, nil 50 | } 51 | -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/node/node.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | ) 7 | 8 | type ModuleInterface interface { 9 | RunEnabledScript(context.Context, string, []string, map[string]string) (bool, error) 10 | GetName() string 11 | GetOrder() uint32 12 | GetPath() string 13 | } 14 | 15 | type NodeType string 16 | 17 | type NodeWeight uint32 18 | 19 | func (weight NodeWeight) String() string { 20 | return strconv.FormatUint(uint64(weight), 10) 21 | } 22 | 23 | func (weight NodeWeight) Int() int { 24 | return int(weight) 25 | } 26 | 27 | type Node struct { 28 | name string 29 | weight NodeWeight 30 | typ NodeType 31 | enabled bool 32 | updatedBy string 33 | module ModuleInterface 34 | } 35 | 36 | const ( 37 | ModuleType NodeType = "module" 38 | WeightType NodeType = "weight" 39 | TypeAttribute string = "type" 40 | ) 41 | 42 | func NewNode() *Node { 43 | return &Node{} 44 | } 45 | 46 | func (n *Node) WithName(name string) *Node { 47 | n.name = name 48 | return n 49 | } 50 | 51 | func (n *Node) WithWeight(order uint32) *Node { 52 | n.weight = NodeWeight(order) 53 | return n 54 | } 55 | 56 | func (n *Node) WithModule(module ModuleInterface) *Node { 57 | n.module = module 58 | return n 59 | } 60 | 61 | func (n *Node) WithType(typ NodeType) *Node { 62 | n.typ = typ 63 | return n 64 | } 65 | 66 | func (n Node) GetName() string { 67 | return n.name 68 | } 69 | 70 | func (n Node) GetWeight() NodeWeight { 71 | return n.weight 72 | } 73 | 74 | func (n Node) IsEnabled() bool { 75 | return n.enabled 76 | } 77 | 78 | func (n Node) GetType() NodeType { 79 | return n.typ 80 | } 81 | 82 | func (n Node) GetModule() ModuleInterface { 83 | return n.module 84 | } 85 | 86 | func (n Node) GetUpdatedBy() string { 87 | return n.updatedBy 88 | } 89 | 90 | func (n *Node) SetState(enabled bool) { 91 | n.enabled = enabled 92 | } 93 | 94 | func (n *Node) SetUpdatedBy(updatedBy string) { 95 | n.updatedBy = updatedBy 96 | } 97 | -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/node/node_test.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | node_mock "github.com/flant/addon-operator/pkg/module_manager/scheduler/node/mock" 9 | ) 10 | 11 | func TestNewNode(t *testing.T) { 12 | n := NewNode() 13 | assert.Equal(t, &Node{}, n) 14 | 15 | m := node_mock.MockModule{ 16 | Name: "test-node", 17 | Order: 32, 18 | } 19 | 20 | n = NewNode().WithName("test-node").WithWeight(uint32(32)).WithType(ModuleType).WithModule(m) 21 | assert.Equal(t, &Node{ 22 | name: "test-node", 23 | weight: 32, 24 | typ: ModuleType, 25 | module: m, 26 | }, n) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/testdata/015-admission-policy-engine/enabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/scheduler/testdata/015-admission-policy-engine/enabled -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/testdata/042-kube-dns/enabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/scheduler/testdata/042-kube-dns/enabled -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/testdata/133-foo-bar/enabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/scheduler/testdata/133-foo-bar/enabled -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/testdata/20-cert-manager/enabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/scheduler/testdata/20-cert-manager/enabled -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/testdata/30-openstack-cloud-provider/enabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/scheduler/testdata/30-openstack-cloud-provider/enabled -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/testdata/340-monitoring-applications/enabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/scheduler/testdata/340-monitoring-applications/enabled -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/testdata/402-ingress-nginx/enabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/scheduler/testdata/402-ingress-nginx/enabled -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/testdata/450-flant-integration/enabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/scheduler/testdata/450-flant-integration/enabled -------------------------------------------------------------------------------- /pkg/module_manager/scheduler/testdata/909-test-echo/enabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/scheduler/testdata/909-test-echo/enabled -------------------------------------------------------------------------------- /pkg/module_manager/testdata/get__global_hook/global-hooks/000-all-bindings/hook: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | cat << EOF 5 | configVersion: v1 6 | onStartup: 1 7 | afterAll: 1 8 | beforeAll: 1 9 | schedule: 10 | - crontab: "* * * * *" 11 | allowFailure: true 12 | 13 | kubernetes: 14 | - executeHookOnEvent: ["Added"] 15 | kind: "configmap" 16 | labelSelector: 17 | matchLabels: 18 | component: component1 19 | matchExpressions: 20 | - key: "tier" 21 | operator: "In" 22 | values: ["cache"] 23 | namespace: 24 | nameSelector: 25 | matchNames: ["namespace1"] 26 | jqFilter: ".items[] | del(.metadata, .field1)" 27 | allowFailure: true 28 | 29 | - kind: "namespace" 30 | labelSelector: 31 | matchLabels: 32 | component: component2 33 | matchExpressions: 34 | - key: "tier" 35 | operator: "In" 36 | values: ["cache"] 37 | namespace: 38 | nameSelector: 39 | matchNames: ["namespace2"] 40 | jqFilter: ".items[] | del(.metadata, .field2)" 41 | allowFailure: true 42 | 43 | - kind: "pod" 44 | labelSelector: 45 | matchLabels: 46 | component: component3 47 | matchExpressions: 48 | - key: "tier" 49 | operator: "In" 50 | values: ["cache"] 51 | namespace: 52 | nameSelector: 53 | matchNames: ["namespace2"] 54 | jqFilter: ".items[] | del(.metadata, .field3)" 55 | EOF 56 | fi 57 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/get__global_hook/global-hooks/100-nested-hook/sub/sub/hook: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | echo " 5 | { 6 | \"beforeAll\": 1 7 | } 8 | " 9 | fi 10 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/get__global_hook/modules/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/testdata/get__global_hook/modules/.gitkeep -------------------------------------------------------------------------------- /pkg/module_manager/testdata/get__global_hooks_in_order/global-hooks/000-before-all-binding-hooks/a: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | echo '{ "afterAll": 4 }' 5 | fi 6 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/get__global_hooks_in_order/global-hooks/000-before-all-binding-hooks/b: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | echo '{ "afterAll": 2 }' 5 | fi 6 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/get__global_hooks_in_order/global-hooks/000-before-all-binding-hooks/c: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | echo '{"afterAll": 3}' 5 | fi 6 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/get__global_hooks_in_order/modules/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/testdata/get__global_hooks_in_order/modules/.gitkeep -------------------------------------------------------------------------------- /pkg/module_manager/testdata/get__module/modules/000-module/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/testdata/get__module/modules/000-module/.gitkeep -------------------------------------------------------------------------------- /pkg/module_manager/testdata/get__module_hook/modules/000-all-bindings/hooks/all-bindings: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | cat << EOF 5 | configVersion: v1 6 | onStartup: 1 7 | afterHelm: 1 8 | beforeHelm: 1 9 | afterDeleteHelm: 1 10 | 11 | schedule: 12 | - crontab: "* * * * *" 13 | allowFailure: true 14 | 15 | kubernetes: 16 | - executeHookOnEvent: ["Added"] 17 | kind: "configmap" 18 | labelSelector: 19 | matchLabels: 20 | component: component1 21 | matchExpressions: 22 | - key: "tier" 23 | operator: "In" 24 | values: ["cache"] 25 | namespace: 26 | nameSelector: 27 | matchNames: ["namespace1"] 28 | jqFilter: ".items[] | del(.metadata, .field1)" 29 | allowFailure: true 30 | 31 | - kind: "namespace" 32 | labelSelector: 33 | matchLabels: 34 | component: component2 35 | matchExpressions: 36 | - key: "tier" 37 | operator: "In" 38 | values: ["cache"] 39 | namespace: 40 | nameSelector: 41 | matchNames: ["namespace2"] 42 | jqFilter: ".items[] | del(.metadata, .field2)" 43 | allowFailure: true 44 | 45 | - kind: "pod" 46 | labelSelector: 47 | matchLabels: 48 | component: component3 49 | matchExpressions: 50 | - key: "tier" 51 | operator: "In" 52 | values: ["cache"] 53 | namespace: 54 | nameSelector: 55 | matchNames: ["namespace2"] 56 | jqFilter: ".items[] | del(.metadata, .field3)" 57 | EOF 58 | fi 59 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/get__module_hook/modules/100-nested-hooks/hooks/sub/sub/nested-before-helm: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | cat < $MODULE_ENABLED_RESULT 4 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/modules_state__no_cm__with_enabled_scripts/modules/002-beta/enabled: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo false > $MODULE_ENABLED_RESULT 4 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/modules_state__no_cm__with_enabled_scripts/modules/003-gamma/enabled: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # dependency: alpha 4 | 5 | # enabled if alpha is enabled 6 | 7 | cat ${VALUES_PATH:-/dev/null} | jq '.global.enabledModules | any(in({"alpha":1}))' > $MODULE_ENABLED_RESULT 8 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/modules_state__no_cm__with_enabled_scripts/modules/004-delta/enabled: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # dependency: alpha 4 | # enabled if alpha is enabled 5 | 6 | cat ${VALUES_PATH:-/dev/null} | jq '.global.enabledModules | any(in({"alpha":1}))' > $MODULE_ENABLED_RESULT 7 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/modules_state__no_cm__with_enabled_scripts/modules/005-epsilon/enabled: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo true > $MODULE_ENABLED_RESULT 4 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/modules_state__no_cm__with_enabled_scripts/modules/006-zeta/enabled: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # dependency: gamma 4 | # dependency: delta 5 | 6 | # enabled if gamma and delta are enabled 7 | 8 | cat ${VALUES_PATH:-/dev/null} | jq '.global.enabledModules | any(in({"gamma":1,"delta":1}))' > $MODULE_ENABLED_RESULT 9 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/modules_state__no_cm__with_enabled_scripts/modules/007-eta/enabled: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo true > $MODULE_ENABLED_RESULT 4 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/modules_state__no_cm__with_enabled_scripts/modules/values.yaml: -------------------------------------------------------------------------------- 1 | alphaEnabled: true 2 | betaEnabled: true 3 | gammaEnabled: true 4 | deltaEnabled: true 5 | epsilonEnabled: true 6 | zetaEnabled: true 7 | etaEnabled: true 8 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_delete_module/modules/000-module/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_delete_module/modules/000-module/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Kubernetes 4 | name: 000-module 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_delete_module/modules/000-module/hooks/hook-1: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | cat < "$VALUES_JSON_PATCH_PATH" 10 | [ 11 | { "op": "add", "path": "/module/afterDeleteHelm", "value": "value-from-after-delete-helm-20" } 12 | ] 13 | EOF 14 | fi 15 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_delete_module/modules/000-module/hooks/hook-2: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | cat < "$VALUES_JSON_PATCH_PATH" 11 | [ 12 | { "op": "add", "path": "/module/afterDeleteHelm", "value": "value-from-after-delete-helm-10" } 13 | ] 14 | EOF 15 | fi 16 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_delete_module/modules/000-module/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range .Values.ingress.hosts }} 4 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} 5 | {{- end }} 6 | {{- else if contains "NodePort" .Values.service.type }} 7 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "000-module.fullname" . }}) 8 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 9 | echo http://$NODE_IP:$NODE_PORT 10 | {{- else if contains "LoadBalancer" .Values.service.type }} 11 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 12 | You can watch the status of by running 'kubectl get svc -w {{ template "000-module.fullname" . }}' 13 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "000-module.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 14 | echo http://$SERVICE_IP:{{ .Values.service.port }} 15 | {{- else if contains "ClusterIP" .Values.service.type }} 16 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "000-module.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 17 | echo "Visit http://127.0.0.1:8080 to use your application" 18 | kubectl port-forward $POD_NAME 8080:80 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_delete_module/modules/000-module/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "000-module.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "000-module.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "000-module.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_delete_module/modules/000-module/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta2 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "000-module.fullname" . }} 5 | labels: 6 | app: {{ template "000-module.name" . }} 7 | chart: {{ template "000-module.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | spec: 11 | replicas: {{ .Values.replicaCount }} 12 | selector: 13 | matchLabels: 14 | app: {{ template "000-module.name" . }} 15 | release: {{ .Release.Name }} 16 | template: 17 | metadata: 18 | labels: 19 | app: {{ template "000-module.name" . }} 20 | release: {{ .Release.Name }} 21 | spec: 22 | containers: 23 | - name: {{ .Chart.Name }} 24 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 25 | imagePullPolicy: {{ .Values.image.pullPolicy }} 26 | ports: 27 | - name: http 28 | containerPort: 80 29 | protocol: TCP 30 | livenessProbe: 31 | httpGet: 32 | path: / 33 | port: http 34 | readinessProbe: 35 | httpGet: 36 | path: / 37 | port: http 38 | resources: 39 | {{ toYaml .Values.resources | indent 12 }} 40 | {{- with .Values.nodeSelector }} 41 | nodeSelector: 42 | {{ toYaml . | indent 8 }} 43 | {{- end }} 44 | {{- with .Values.affinity }} 45 | affinity: 46 | {{ toYaml . | indent 8 }} 47 | {{- end }} 48 | {{- with .Values.tolerations }} 49 | tolerations: 50 | {{ toYaml . | indent 8 }} 51 | {{- end }} 52 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_delete_module/modules/000-module/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "000-module.fullname" . -}} 3 | {{- $servicePort := .Values.service.port -}} 4 | {{- $ingressPath := .Values.ingress.path -}} 5 | apiVersion: extensions/v1beta1 6 | kind: Ingress 7 | metadata: 8 | name: {{ $fullName }} 9 | labels: 10 | app: {{ template "000-module.name" . }} 11 | chart: {{ template "000-module.chart" . }} 12 | release: {{ .Release.Name }} 13 | heritage: {{ .Release.Service }} 14 | {{- with .Values.ingress.annotations }} 15 | annotations: 16 | {{ toYaml . | indent 4 }} 17 | {{- end }} 18 | spec: 19 | {{- if .Values.ingress.tls }} 20 | tls: 21 | {{- range .Values.ingress.tls }} 22 | - hosts: 23 | {{- range .hosts }} 24 | - {{ . }} 25 | {{- end }} 26 | secretName: {{ .secretName }} 27 | {{- end }} 28 | {{- end }} 29 | rules: 30 | {{- range .Values.ingress.hosts }} 31 | - host: {{ . }} 32 | http: 33 | paths: 34 | - path: {{ $ingressPath }} 35 | backend: 36 | serviceName: {{ $fullName }} 37 | servicePort: http 38 | {{- end }} 39 | {{- end }} 40 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_delete_module/modules/000-module/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "000-module.fullname" . }} 5 | labels: 6 | app: {{ template "000-module.name" . }} 7 | chart: {{ template "000-module.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | spec: 11 | type: {{ .Values.service.type }} 12 | ports: 13 | - port: {{ .Values.service.port }} 14 | targetPort: http 15 | protocol: TCP 16 | name: http 17 | selector: 18 | app: {{ template "000-module.name" . }} 19 | release: {{ .Release.Name }} 20 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_delete_module/modules/000-module/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for 000-module. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | module: 6 | imageName: "nginx:stable" 7 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_global_hook/global-hooks/000-update-kube-config/merge_and_patch_values: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | cat < "$CONFIG_VALUES_JSON_PATCH_PATH" 10 | [ 11 | { "op": "add", "path": "/global/a", "value": 2 }, 12 | { "op": "remove", "path": "/global/b" }, 13 | { "op": "add", "path": "/global/c", "value": [3] } 14 | ] 15 | EOF 16 | fi 17 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_global_hook/global-hooks/100-update-dynamic/merge_and_patch_values: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | cat < "$VALUES_JSON_PATCH_PATH" 10 | [ 11 | { "op": "add", "path": "/global/a", "value": 9 }, 12 | { "op": "add", "path": "/global/c", "value": "10" } 13 | ] 14 | EOF 15 | fi 16 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_global_hook/modules/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flant/addon-operator/74df397cd4ffe42e73b915009a543e491f581ab6/pkg/module_manager/testdata/test_run_global_hook/modules/.gitkeep -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_module/modules/000-module/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_module/modules/000-module/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Kubernetes 4 | name: 000-module 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_module/modules/000-module/hooks/hook-1: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | cat < "$VALUES_JSON_PATCH_PATH" 10 | [ 11 | { "op": "add", "path": "/module/beforeHelm", "value": "value-from-before-helm-20" } 12 | ] 13 | EOF 14 | fi 15 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_module/modules/000-module/hooks/hook-2: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | cat < "$VALUES_JSON_PATCH_PATH" 10 | [ 11 | { "op": "add", "path": "/module/afterHelm", "value": "value-from-before-helm-2" } 12 | ] 13 | EOF 14 | fi 15 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_module/modules/000-module/hooks/hook-3: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | cat < "$VALUES_JSON_PATCH_PATH" 10 | [ 11 | { "op": "add", "path": "/module/beforeHelm", "value": "value-from-before-helm-1" } 12 | ] 13 | EOF 14 | fi 15 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_module/modules/000-module/hooks/hook-4: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | cat < "$VALUES_JSON_PATCH_PATH" 10 | [ 11 | { "op": "add", "path": "/module/afterHelm", "value": "value-from-after-helm" } 12 | ] 13 | EOF 14 | fi 15 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_module/modules/000-module/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range .Values.ingress.hosts }} 4 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} 5 | {{- end }} 6 | {{- else if contains "NodePort" .Values.service.type }} 7 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "000-module.fullname" . }}) 8 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 9 | echo http://$NODE_IP:$NODE_PORT 10 | {{- else if contains "LoadBalancer" .Values.service.type }} 11 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 12 | You can watch the status of by running 'kubectl get svc -w {{ template "000-module.fullname" . }}' 13 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "000-module.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 14 | echo http://$SERVICE_IP:{{ .Values.service.port }} 15 | {{- else if contains "ClusterIP" .Values.service.type }} 16 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "000-module.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 17 | echo "Visit http://127.0.0.1:8080 to use your application" 18 | kubectl port-forward $POD_NAME 8080:80 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_module/modules/000-module/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "000-module.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "000-module.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "000-module.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_module/modules/000-module/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta2 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "000-module.fullname" . }} 5 | labels: 6 | app: {{ template "000-module.name" . }} 7 | chart: {{ template "000-module.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | spec: 11 | replicas: {{ .Values.replicaCount }} 12 | selector: 13 | matchLabels: 14 | app: {{ template "000-module.name" . }} 15 | release: {{ .Release.Name }} 16 | template: 17 | metadata: 18 | labels: 19 | app: {{ template "000-module.name" . }} 20 | release: {{ .Release.Name }} 21 | spec: 22 | containers: 23 | - name: {{ .Chart.Name }} 24 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 25 | imagePullPolicy: {{ .Values.image.pullPolicy }} 26 | ports: 27 | - name: http 28 | containerPort: 80 29 | protocol: TCP 30 | livenessProbe: 31 | httpGet: 32 | path: / 33 | port: http 34 | readinessProbe: 35 | httpGet: 36 | path: / 37 | port: http 38 | resources: 39 | {{ toYaml .Values.resources | indent 12 }} 40 | {{- with .Values.nodeSelector }} 41 | nodeSelector: 42 | {{ toYaml . | indent 8 }} 43 | {{- end }} 44 | {{- with .Values.affinity }} 45 | affinity: 46 | {{ toYaml . | indent 8 }} 47 | {{- end }} 48 | {{- with .Values.tolerations }} 49 | tolerations: 50 | {{ toYaml . | indent 8 }} 51 | {{- end }} 52 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_module/modules/000-module/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "000-module.fullname" . -}} 3 | {{- $servicePort := .Values.service.port -}} 4 | {{- $ingressPath := .Values.ingress.path -}} 5 | apiVersion: extensions/v1beta1 6 | kind: Ingress 7 | metadata: 8 | name: {{ $fullName }} 9 | labels: 10 | app: {{ template "000-module.name" . }} 11 | chart: {{ template "000-module.chart" . }} 12 | release: {{ .Release.Name }} 13 | heritage: {{ .Release.Service }} 14 | {{- with .Values.ingress.annotations }} 15 | annotations: 16 | {{ toYaml . | indent 4 }} 17 | {{- end }} 18 | spec: 19 | {{- if .Values.ingress.tls }} 20 | tls: 21 | {{- range .Values.ingress.tls }} 22 | - hosts: 23 | {{- range .hosts }} 24 | - {{ . }} 25 | {{- end }} 26 | secretName: {{ .secretName }} 27 | {{- end }} 28 | {{- end }} 29 | rules: 30 | {{- range .Values.ingress.hosts }} 31 | - host: {{ . }} 32 | http: 33 | paths: 34 | - path: {{ $ingressPath }} 35 | backend: 36 | serviceName: {{ $fullName }} 37 | servicePort: http 38 | {{- end }} 39 | {{- end }} 40 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_module/modules/000-module/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "000-module.fullname" . }} 5 | labels: 6 | app: {{ template "000-module.name" . }} 7 | chart: {{ template "000-module.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | spec: 11 | type: {{ .Values.service.type }} 12 | ports: 13 | - port: {{ .Values.service.port }} 14 | targetPort: http 15 | protocol: TCP 16 | name: http 17 | selector: 18 | app: {{ template "000-module.name" . }} 19 | release: {{ .Release.Name }} 20 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_module/modules/000-module/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for 000-module. 2 | module: 3 | imageName: "nginx:stable" 4 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_module_hook/modules/000-update-kube-module-config/hooks/merge_and_patch_values: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | cat < "$CONFIG_VALUES_JSON_PATCH_PATH" 10 | [ 11 | { "op": "add", "path": "/updateKubeModuleConfig/a", "value": 2 }, 12 | { "op": "remove", "path": "/updateKubeModuleConfig/b" }, 13 | { "op": "add", "path": "/updateKubeModuleConfig/c", "value": [3] } 14 | ] 15 | EOF 16 | fi 17 | -------------------------------------------------------------------------------- /pkg/module_manager/testdata/test_run_module_hook/modules/100-update-module-dynamic/hooks/merge_and_patch_values: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ "$1" == "--config" ]]; then 4 | cat < "$VALUES_JSON_PATCH_PATH" 10 | [ 11 | { "op": "add", "path": "/updateModuleDynamic/a", "value": 9 }, 12 | { "op": "add", "path": "/updateModuleDynamic/c", "value": "10" } 13 | ] 14 | EOF 15 | fi 16 | -------------------------------------------------------------------------------- /pkg/task/discover-crds/service.go: -------------------------------------------------------------------------------- 1 | package discovercrds 2 | 3 | import "sync" 4 | 5 | type DiscoveredGVKs struct { 6 | mu sync.Mutex 7 | discoveredGVKs map[string]struct{} 8 | } 9 | 10 | func NewDiscoveredGVKs() *DiscoveredGVKs { 11 | return &DiscoveredGVKs{ 12 | discoveredGVKs: make(map[string]struct{}), 13 | } 14 | } 15 | 16 | func (d *DiscoveredGVKs) AddGVK(crds ...string) { 17 | d.mu.Lock() 18 | defer d.mu.Unlock() 19 | 20 | for _, crd := range crds { 21 | d.discoveredGVKs[crd] = struct{}{} 22 | } 23 | } 24 | 25 | func (d *DiscoveredGVKs) ProcessGVKs(processor func(crdList []string)) { 26 | d.mu.Lock() 27 | defer d.mu.Unlock() 28 | 29 | if len(d.discoveredGVKs) == 0 { 30 | return 31 | } 32 | 33 | gvkList := make([]string, 0, len(d.discoveredGVKs)) 34 | for gvk := range d.discoveredGVKs { 35 | gvkList = append(gvkList, gvk) 36 | } 37 | 38 | processor(gvkList) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/task/global-hook-enable-schedule-bindings/task.go: -------------------------------------------------------------------------------- 1 | package globalhookenableschedulebindings 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/deckhouse/deckhouse/pkg/log" 7 | "go.opentelemetry.io/otel" 8 | 9 | "github.com/flant/addon-operator/pkg/module_manager" 10 | "github.com/flant/addon-operator/pkg/task" 11 | sh_task "github.com/flant/shell-operator/pkg/task" 12 | "github.com/flant/shell-operator/pkg/task/queue" 13 | ) 14 | 15 | const ( 16 | taskName = "global-hook-enable-schedule-bindings" 17 | ) 18 | 19 | type TaskDependencies interface { 20 | GetModuleManager() *module_manager.ModuleManager 21 | } 22 | 23 | // Task represents a handler for enabling schedule bindings on global hooks 24 | type Task struct { 25 | shellTask sh_task.Task 26 | moduleManager *module_manager.ModuleManager 27 | logger *log.Logger 28 | } 29 | 30 | // RegisterTaskHandler returns a function that creates a Task handler 31 | func RegisterTaskHandler(config TaskDependencies) func(t sh_task.Task, logger *log.Logger) task.Task { 32 | return func(t sh_task.Task, logger *log.Logger) task.Task { 33 | return NewTask( 34 | t, 35 | config.GetModuleManager(), 36 | logger.Named("global-hook-enable-schedule-bindings"), 37 | ) 38 | } 39 | } 40 | 41 | // NewTask creates a new Task instance. 42 | func NewTask(shellTask sh_task.Task, moduleManager *module_manager.ModuleManager, logger *log.Logger) *Task { 43 | return &Task{ 44 | shellTask: shellTask, 45 | moduleManager: moduleManager, 46 | logger: logger, 47 | } 48 | } 49 | 50 | func (s *Task) Handle(ctx context.Context) queue.TaskResult { 51 | _, span := otel.Tracer(taskName).Start(ctx, "handle") 52 | defer span.End() 53 | 54 | result := queue.TaskResult{} 55 | 56 | hm := task.HookMetadataAccessor(s.shellTask) 57 | 58 | globalHook := s.moduleManager.GetGlobalHook(hm.HookName) 59 | globalHook.GetHookController().EnableScheduleBindings() 60 | 61 | result.Status = queue.Success 62 | 63 | return result 64 | } 65 | -------------------------------------------------------------------------------- /pkg/task/global-hook-wait-kubernetes-synchronization/task.go: -------------------------------------------------------------------------------- 1 | package globalhookwaitkubernetessynchronization 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/deckhouse/deckhouse/pkg/log" 8 | "go.opentelemetry.io/otel" 9 | 10 | "github.com/flant/addon-operator/pkg/module_manager" 11 | "github.com/flant/addon-operator/pkg/task" 12 | sh_task "github.com/flant/shell-operator/pkg/task" 13 | "github.com/flant/shell-operator/pkg/task/queue" 14 | ) 15 | 16 | const ( 17 | taskName = "global-hook-wait-kubernetes-synchronization" 18 | ) 19 | 20 | // TaskDependencies defines the interface for accessing necessary components 21 | type TaskDependencies interface { 22 | GetModuleManager() *module_manager.ModuleManager 23 | } 24 | 25 | // RegisterTaskHandler creates a factory function for global hook wait kubernetes synchronization tasks 26 | func RegisterTaskHandler(svc TaskDependencies) func(t sh_task.Task, logger *log.Logger) task.Task { 27 | return func(t sh_task.Task, logger *log.Logger) task.Task { 28 | return NewTask( 29 | t, 30 | svc.GetModuleManager(), 31 | logger.Named("global-hook-wait-kubernetes-synchronization"), 32 | ) 33 | } 34 | } 35 | 36 | // Task handles waiting for kubernetes synchronization for global hooks 37 | type Task struct { 38 | shellTask sh_task.Task 39 | moduleManager *module_manager.ModuleManager 40 | logger *log.Logger 41 | } 42 | 43 | // NewTask creates a new task handler for global hook wait kubernetes synchronization 44 | func NewTask( 45 | shellTask sh_task.Task, 46 | moduleManager *module_manager.ModuleManager, 47 | logger *log.Logger, 48 | ) *Task { 49 | return &Task{ 50 | shellTask: shellTask, 51 | moduleManager: moduleManager, 52 | logger: logger, 53 | } 54 | } 55 | 56 | func (s *Task) Handle(ctx context.Context) queue.TaskResult { 57 | _, span := otel.Tracer(taskName).Start(ctx, "handle") 58 | defer span.End() 59 | 60 | res := queue.TaskResult{ 61 | Status: queue.Success, 62 | } 63 | 64 | if s.moduleManager.GlobalSynchronizationNeeded() && !s.moduleManager.GlobalSynchronizationState().IsCompleted() { 65 | // dump state 66 | s.moduleManager.GlobalSynchronizationState().DebugDumpState(s.logger) 67 | s.shellTask.WithQueuedAt(time.Now()) 68 | 69 | res.Status = queue.Repeat 70 | } else { 71 | s.logger.Info("Synchronization done for all global hooks") 72 | } 73 | 74 | return res 75 | } 76 | -------------------------------------------------------------------------------- /pkg/task/module-purge/task.go: -------------------------------------------------------------------------------- 1 | package modulepurge 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/deckhouse/deckhouse/pkg/log" 7 | "go.opentelemetry.io/otel" 8 | 9 | "github.com/flant/addon-operator/pkg/helm" 10 | "github.com/flant/addon-operator/pkg/task" 11 | sh_task "github.com/flant/shell-operator/pkg/task" 12 | "github.com/flant/shell-operator/pkg/task/queue" 13 | ) 14 | 15 | const ( 16 | taskName = "module-purge" 17 | ) 18 | 19 | // TaskDependencies defines the interface for accessing necessary components 20 | type TaskDependencies interface { 21 | GetHelm() *helm.ClientFactory 22 | } 23 | 24 | // RegisterTaskHandler creates a factory function for ModulePurge tasks 25 | func RegisterTaskHandler(svc TaskDependencies) func(t sh_task.Task, logger *log.Logger) task.Task { 26 | return func(t sh_task.Task, logger *log.Logger) task.Task { 27 | return NewTask( 28 | t, 29 | svc.GetHelm(), 30 | logger.Named("module-purge"), 31 | ) 32 | } 33 | } 34 | 35 | // Task handles purging modules 36 | type Task struct { 37 | shellTask sh_task.Task 38 | helm *helm.ClientFactory 39 | logger *log.Logger 40 | } 41 | 42 | // NewTask creates a new task handler for module purging 43 | func NewTask( 44 | shellTask sh_task.Task, 45 | helm *helm.ClientFactory, 46 | logger *log.Logger, 47 | ) *Task { 48 | return &Task{ 49 | shellTask: shellTask, 50 | helm: helm, 51 | logger: logger, 52 | } 53 | } 54 | 55 | func (s *Task) Handle(ctx context.Context) queue.TaskResult { 56 | _, span := otel.Tracer(taskName).Start(ctx, "handle") 57 | defer span.End() 58 | 59 | var res queue.TaskResult 60 | 61 | s.logger.Debug("Module purge start") 62 | 63 | hm := task.HookMetadataAccessor(s.shellTask) 64 | helmClientOptions := []helm.ClientOption{ 65 | helm.WithLogLabels(s.shellTask.GetLogLabels()), 66 | } 67 | 68 | err := s.helm.NewClient(s.logger.Named("helm-client"), helmClientOptions...).DeleteRelease(hm.ModuleName) 69 | if err != nil { 70 | // Purge is for unknown modules, just print warning. 71 | s.logger.Warn("Module purge failed, no retry.", log.Err(err)) 72 | } else { 73 | s.logger.Debug("Module purge success") 74 | } 75 | 76 | res.Status = queue.Success 77 | 78 | return res 79 | } 80 | -------------------------------------------------------------------------------- /pkg/task/parallel/parallel.go: -------------------------------------------------------------------------------- 1 | package parallel 2 | 3 | import "sync" 4 | 5 | type queueEvent struct { 6 | moduleName string 7 | errMsg string 8 | succeeded bool 9 | } 10 | 11 | func (e queueEvent) ModuleName() string { 12 | return e.moduleName 13 | } 14 | 15 | func (e queueEvent) ErrorMessage() string { 16 | return e.errMsg 17 | } 18 | 19 | func (e queueEvent) Succeeded() bool { 20 | return e.succeeded 21 | } 22 | 23 | type TaskChannel chan queueEvent 24 | 25 | func NewTaskChannel() TaskChannel { 26 | return TaskChannel(make(chan queueEvent)) 27 | } 28 | 29 | func (t TaskChannel) SendSuccess(moduleName string) { 30 | t <- queueEvent{ 31 | moduleName: moduleName, 32 | succeeded: true, 33 | } 34 | } 35 | 36 | func (t TaskChannel) SendFailure(moduleName string, errMsg string) { 37 | t <- queueEvent{ 38 | moduleName: moduleName, 39 | errMsg: errMsg, 40 | succeeded: false, 41 | } 42 | } 43 | 44 | type TaskChannels struct { 45 | l sync.Mutex 46 | channels map[string]TaskChannel 47 | } 48 | 49 | func NewTaskChannels() *TaskChannels { 50 | return &TaskChannels{ 51 | channels: make(map[string]TaskChannel), 52 | } 53 | } 54 | 55 | func (pq *TaskChannels) Channels() []string { 56 | pq.l.Lock() 57 | defer pq.l.Unlock() 58 | 59 | ids := make([]string, 0, len(pq.channels)) 60 | 61 | for id := range pq.channels { 62 | ids = append(ids, id) 63 | } 64 | 65 | return ids 66 | } 67 | 68 | func (pq *TaskChannels) Set(id string, c TaskChannel) { 69 | pq.l.Lock() 70 | pq.channels[id] = c 71 | pq.l.Unlock() 72 | } 73 | 74 | func (pq *TaskChannels) Get(id string) (TaskChannel, bool) { 75 | pq.l.Lock() 76 | defer pq.l.Unlock() 77 | c, ok := pq.channels[id] 78 | return c, ok 79 | } 80 | 81 | func (pq *TaskChannels) Delete(id string) { 82 | pq.l.Lock() 83 | delete(pq.channels, id) 84 | pq.l.Unlock() 85 | } 86 | -------------------------------------------------------------------------------- /pkg/task/service/logs.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "github.com/deckhouse/deckhouse/pkg/log" 8 | 9 | "github.com/flant/addon-operator/pkg" 10 | "github.com/flant/addon-operator/pkg/module_manager/models/modules" 11 | "github.com/flant/addon-operator/pkg/task" 12 | "github.com/flant/addon-operator/pkg/task/helpers" 13 | sh_task "github.com/flant/shell-operator/pkg/task" 14 | "github.com/flant/shell-operator/pkg/task/queue" 15 | ) 16 | 17 | // logTaskStart prints info about task at start. Also prints event source info from task props. 18 | func (s *TaskHandlerService) logTaskStart(tsk sh_task.Task, logger *log.Logger) { 19 | // Prevent excess messages for highly frequent tasks. 20 | if tsk.GetType() == task.GlobalHookWaitKubernetesSynchronization { 21 | return 22 | } 23 | 24 | if tsk.GetType() == task.ModuleRun { 25 | hm := task.HookMetadataAccessor(tsk) 26 | baseModule := s.moduleManager.GetModule(hm.ModuleName) 27 | 28 | if baseModule.GetPhase() == modules.WaitForSynchronization { 29 | return 30 | } 31 | } 32 | 33 | logger = logger.With(pkg.LogKeyTaskFlow, "start") 34 | 35 | if triggeredBy, ok := tsk.GetProp("triggered-by").([]slog.Attr); ok { 36 | for _, attr := range triggeredBy { 37 | logger = logger.With(attr) 38 | } 39 | } 40 | 41 | logger.Info(helpers.TaskDescriptionForTaskFlowLog(tsk, "start", s.taskPhase(tsk), "")) 42 | } 43 | 44 | // logTaskEnd prints info about task at the end. Info level used only for the ConvergeModules task. 45 | func (s *TaskHandlerService) logTaskEnd(tsk sh_task.Task, result queue.TaskResult, logger *log.Logger) { 46 | logger = logger.With(pkg.LogKeyTaskFlow, "end") 47 | 48 | level := log.LevelDebug 49 | if tsk.GetType() == task.ConvergeModules { 50 | level = log.LevelInfo 51 | } 52 | 53 | logger.Log(context.TODO(), level.Level(), helpers.TaskDescriptionForTaskFlowLog(tsk, "end", s.taskPhase(tsk), string(result.Status))) 54 | } 55 | 56 | func (s *TaskHandlerService) taskPhase(tsk sh_task.Task) string { 57 | switch tsk.GetType() { 58 | case task.ConvergeModules: 59 | // return string(s.ConvergeState.Phase) 60 | case task.ModuleRun: 61 | hm := task.HookMetadataAccessor(tsk) 62 | mod := s.moduleManager.GetModule(hm.ModuleName) 63 | return string(mod.GetPhase()) 64 | } 65 | return "" 66 | } 67 | -------------------------------------------------------------------------------- /pkg/task/service/metric.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/flant/addon-operator/pkg" 7 | "github.com/flant/addon-operator/pkg/task" 8 | sh_task "github.com/flant/shell-operator/pkg/task" 9 | ) 10 | 11 | // UpdateWaitInQueueMetric increases task_wait_in_queue_seconds_total counter for the task type. 12 | // TODO pass queue name from handler, not from task 13 | func (s *TaskHandlerService) UpdateWaitInQueueMetric(t sh_task.Task) { 14 | metricLabels := map[string]string{ 15 | "module": "", 16 | "hook": "", 17 | pkg.MetricKeyBinding: string(t.GetType()), 18 | "queue": t.GetQueueName(), 19 | } 20 | 21 | hm := task.HookMetadataAccessor(t) 22 | 23 | switch t.GetType() { 24 | case task.GlobalHookRun, 25 | task.GlobalHookEnableScheduleBindings, 26 | task.GlobalHookEnableKubernetesBindings, 27 | task.GlobalHookWaitKubernetesSynchronization: 28 | metricLabels["hook"] = hm.HookName 29 | 30 | case task.ModuleRun, 31 | task.ModuleDelete, 32 | task.ModuleHookRun, 33 | task.ModulePurge: 34 | metricLabels["module"] = hm.ModuleName 35 | 36 | case task.ConvergeModules, 37 | task.DiscoverHelmReleases: 38 | // no action required 39 | } 40 | 41 | if t.GetType() == task.GlobalHookRun { 42 | // set binding name instead of type 43 | metricLabels[pkg.MetricKeyBinding] = hm.Binding 44 | } 45 | if t.GetType() == task.ModuleHookRun { 46 | // set binding name instead of type 47 | metricLabels["hook"] = hm.HookName 48 | metricLabels[pkg.MetricKeyBinding] = hm.Binding 49 | } 50 | 51 | taskWaitTime := time.Since(t.GetQueuedAt()).Seconds() 52 | s.metricStorage.CounterAdd("{PREFIX}task_wait_in_queue_seconds_total", taskWaitTime, metricLabels) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/task/task.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/flant/shell-operator/pkg/task" 7 | "github.com/flant/shell-operator/pkg/task/queue" 8 | ) 9 | 10 | // Addon-operator specific task types 11 | const ( 12 | // GlobalHookRun runs a global hook. 13 | GlobalHookRun task.TaskType = "GlobalHookRun" 14 | // ModuleHookRun runs schedule or kubernetes hook. 15 | ModuleHookRun task.TaskType = "ModuleHookRun" 16 | // ModuleDelete runs helm delete/afterHelmDelete sequence. 17 | ModuleDelete task.TaskType = "ModuleDelete" 18 | // ModuleRun runs beforeHelm/helm upgrade/afterHelm sequence. 19 | ModuleRun task.TaskType = "ModuleRun" 20 | // ParallelModuleRun runs beforeHelm/helm upgrade/afterHelm sequence for a bunch of modules in parallel. 21 | ParallelModuleRun task.TaskType = "ParallelModuleRun" 22 | // ModulePurge - delete unknown helm release (no module in ModulesDir) 23 | ModulePurge task.TaskType = "ModulePurge" 24 | // ModuleEnsureCRDs runs ensureCRDs task for enabled module 25 | ModuleEnsureCRDs task.TaskType = "ModuleEnsureCRDs" 26 | 27 | // DiscoverHelmReleases lists helm releases to detect unknown modules and initiate enabled modules list. 28 | DiscoverHelmReleases task.TaskType = "DiscoverHelmReleases" 29 | 30 | // ConvergeModules runs beforeAll/run modules/afterAll sequence for all enabled modules. 31 | ConvergeModules task.TaskType = "ConvergeModules" 32 | 33 | // ApplyKubeConfigValues validates and updates modules' values 34 | ApplyKubeConfigValues task.TaskType = "ApplyKubeConfigValues" 35 | 36 | GlobalHookEnableKubernetesBindings task.TaskType = "GlobalHookEnableKubernetesBindings" 37 | GlobalHookWaitKubernetesSynchronization task.TaskType = "GlobalHookWaitKubernetesSynchronization" 38 | GlobalHookEnableScheduleBindings task.TaskType = "GlobalHookEnableScheduleBindings" 39 | ) 40 | 41 | type Task interface { 42 | Handle(ctx context.Context) queue.TaskResult 43 | } 44 | -------------------------------------------------------------------------------- /pkg/task/test/task_metadata_test.go: -------------------------------------------------------------------------------- 1 | package task_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/gomega" 7 | 8 | . "github.com/flant/addon-operator/pkg/hook/types" 9 | . "github.com/flant/addon-operator/pkg/task" 10 | "github.com/flant/shell-operator/pkg/hook/types" 11 | sh_task "github.com/flant/shell-operator/pkg/task" 12 | ) 13 | 14 | func Test_MetadataAccessor(tT *testing.T) { 15 | g := NewWithT(tT) 16 | t := sh_task.NewTask(ModuleRun) 17 | 18 | t.WithMetadata(HookMetadata{ 19 | BindingType: BeforeAll, 20 | ModuleName: "module-name", 21 | EventDescription: "ReloadAllTasks", 22 | DoModuleStartup: true, 23 | }) 24 | 25 | hm := HookMetadataAccessor(t) 26 | 27 | g.Expect(hm.ModuleName).Should(Equal("module-name")) 28 | } 29 | 30 | func Test_TaskDescription(t *testing.T) { 31 | tests := []struct { 32 | name string 33 | metadata HookMetadata 34 | expect string 35 | }{ 36 | { 37 | "global hook", 38 | HookMetadata{ 39 | BindingType: BeforeAll, 40 | ModuleName: "module-name", 41 | HookName: "hook.sh", 42 | EventDescription: "ReloadAllTasks", 43 | DoModuleStartup: true, 44 | }, 45 | "beforeAll:hook.sh:ReloadAllTasks", 46 | }, 47 | { 48 | "module run", 49 | HookMetadata{ 50 | ModuleName: "module-name", 51 | EventDescription: "BootstrapMainQueue", 52 | }, 53 | "module-name:BootstrapMainQueue", 54 | }, 55 | { 56 | "module run with DoModuleStartup", 57 | HookMetadata{ 58 | ModuleName: "module-name", 59 | EventDescription: "GlobalValuesChanged", 60 | DoModuleStartup: true, 61 | }, 62 | "module-name:doStartup:GlobalValuesChanged", 63 | }, 64 | { 65 | "module hook", 66 | HookMetadata{ 67 | BindingType: types.OnKubernetesEvent, 68 | ModuleName: "module", 69 | HookName: "module/hook.sh", 70 | EventDescription: "Kubernetes", 71 | }, 72 | "kubernetes:module/hook.sh:Kubernetes", 73 | }, 74 | } 75 | 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | g := NewWithT(t) 79 | g.Expect(tt.metadata.GetDescription()).To(Equal(tt.expect)) 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pkg/utils/chroot.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/flant/addon-operator/pkg/app" 7 | ) 8 | 9 | func GetModuleChrootPath(moduleName string) string { 10 | if len(app.ShellChrootDir) > 0 { 11 | return fmt.Sprintf("%s/%s", app.ShellChrootDir, moduleName) 12 | } 13 | 14 | return "" 15 | } 16 | -------------------------------------------------------------------------------- /pkg/utils/fschecksum.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | "os" 8 | "path" 9 | "sort" 10 | ) 11 | 12 | func CalculateStringsChecksum(stringArr ...string) string { 13 | hasher := md5.New() 14 | sort.Strings(stringArr) 15 | for _, value := range stringArr { 16 | _, _ = hasher.Write([]byte(value)) 17 | } 18 | return hex.EncodeToString(hasher.Sum(nil)) 19 | } 20 | 21 | func CalculateChecksumOfFile(path string) (string, error) { 22 | content, err := os.ReadFile(path) 23 | if err != nil { 24 | return "", err 25 | } 26 | return CalculateStringsChecksum(string(content)), nil 27 | } 28 | 29 | func CalculateChecksumOfDirectory(dir string) (string, error) { 30 | res := "" 31 | 32 | var checkErr error 33 | files, err := FilesFromRoot(dir, func(dir string, name string, _ os.FileInfo) bool { 34 | fPath := path.Join(dir, name) 35 | checksum, err := CalculateChecksumOfFile(fPath) 36 | if err != nil { 37 | // return only bad files for logging 38 | checkErr = err 39 | return true 40 | } 41 | res = CalculateStringsChecksum(res, checksum) 42 | // good files are skipped 43 | return false 44 | }) 45 | if err != nil { 46 | return "", err 47 | } 48 | if checkErr != nil { 49 | return "", fmt.Errorf("calculate checksum of %+v: %v", files, err) 50 | } 51 | 52 | return res, nil 53 | } 54 | 55 | func CalculateChecksumOfPaths(paths ...string) (string, error) { 56 | res := "" 57 | 58 | for _, aPath := range paths { 59 | fileInfo, err := os.Stat(aPath) 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | var checksum string 65 | if fileInfo.IsDir() { 66 | checksum, err = CalculateChecksumOfDirectory(aPath) 67 | } else { 68 | checksum, err = CalculateChecksumOfFile(aPath) 69 | } 70 | 71 | if err != nil { 72 | return "", err 73 | } 74 | res = CalculateStringsChecksum(res, checksum) 75 | } 76 | 77 | return res, nil 78 | } 79 | -------------------------------------------------------------------------------- /pkg/utils/helpers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "os" 4 | 5 | func DumpData(filePath string, data []byte) error { 6 | err := os.WriteFile(filePath, data, 0o644) 7 | if err != nil { 8 | return err 9 | } 10 | return nil 11 | } 12 | 13 | func CreateEmptyWritableFile(filePath string) error { 14 | file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666) 15 | if err != nil { 16 | return nil 17 | } 18 | 19 | _ = file.Close() 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /pkg/utils/merge_labels.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/deckhouse/deckhouse/pkg/log" 7 | ) 8 | 9 | // MergeLabels merges several maps into one. Last map keys overrides keys from first maps. 10 | // 11 | // Can be used to copy a map if just one argument is used. 12 | func MergeLabels(labelsMaps ...map[string]string) map[string]string { 13 | labels := make(map[string]string) 14 | for _, labelsMap := range labelsMaps { 15 | for k, v := range labelsMap { 16 | labels[k] = v 17 | } 18 | } 19 | return labels 20 | } 21 | 22 | func EnrichLoggerWithLabels(logger *log.Logger, labelsMaps ...map[string]string) *log.Logger { 23 | loggerEntry := logger 24 | 25 | for _, labels := range labelsMaps { 26 | for k, v := range labels { 27 | loggerEntry = loggerEntry.With(slog.String(k, v)) 28 | } 29 | } 30 | 31 | return loggerEntry 32 | } 33 | -------------------------------------------------------------------------------- /pkg/utils/mergemap.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | const maxDepth = 32 8 | 9 | // mergeMap recursively merges the src and dst maps. Key conflicts are resolved by 10 | // preferring src, or recursively descending, if both src and dst are maps. 11 | func mergeMap(dst, src map[string]interface{}) map[string]interface{} { 12 | return merge(dst, src, 0) 13 | } 14 | 15 | func merge(dst, src map[string]interface{}, depth int) map[string]interface{} { 16 | if depth > maxDepth { 17 | panic("too deep!") 18 | } 19 | 20 | for key, srcVal := range src { 21 | srcMap, srcMapOk := mapify(srcVal) 22 | if dstVal, ok := dst[key]; ok { 23 | dstMap, dstMapOk := mapify(dstVal) 24 | if srcMapOk && dstMapOk { 25 | dst[key] = merge(dstMap, srcMap, depth+1) 26 | continue 27 | } 28 | } 29 | 30 | if srcMapOk { 31 | dst[key] = deepCopyMap(srcMap) 32 | } else { 33 | dst[key] = srcVal 34 | } 35 | } 36 | return dst 37 | } 38 | 39 | func mapify(i interface{}) (map[string]interface{}, bool) { 40 | switch v := i.(type) { 41 | case map[string]interface{}: 42 | return v, true 43 | case Values: 44 | return v, true 45 | } 46 | 47 | value := reflect.ValueOf(i) 48 | if value.Kind() == reflect.Map { 49 | m := make(map[string]interface{}, value.Len()) 50 | for _, k := range value.MapKeys() { 51 | m[k.String()] = value.MapIndex(k).Interface() 52 | } 53 | return m, true 54 | } 55 | return map[string]interface{}{}, false 56 | } 57 | -------------------------------------------------------------------------------- /pkg/utils/stdliblogtolog/adapter.go: -------------------------------------------------------------------------------- 1 | package stdliblogtolog 2 | 3 | import ( 4 | "context" 5 | "io" 6 | stdlog "log" 7 | "strings" 8 | 9 | "github.com/deckhouse/deckhouse/pkg/log" 10 | logctx "github.com/deckhouse/deckhouse/pkg/log/context" 11 | ) 12 | 13 | func InitAdapter(logger *log.Logger) { 14 | stdlog.SetOutput(&writer{logger: logger.Named("helm")}) 15 | } 16 | 17 | var _ io.Writer = (*writer)(nil) 18 | 19 | type writer struct { 20 | logger *log.Logger 21 | } 22 | 23 | func (w *writer) Write(msg []byte) (int, error) { 24 | // There is no loglevel for stdlib logger 25 | w.logger.Log(logctx.SetCustomKeyContext(context.Background()), log.LevelInfo.Level(), strings.TrimSpace(string(msg))) 26 | 27 | return 0, nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/utils/stdliblogtolog/adapter_test.go: -------------------------------------------------------------------------------- 1 | package stdliblogtolog 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | stdlog "log" 9 | "testing" 10 | 11 | "github.com/deckhouse/deckhouse/pkg/log" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | type testLogLine struct { 16 | Level string `json:"level"` 17 | Message string `json:"msg"` 18 | Logger string `json:"logger"` 19 | } 20 | 21 | func TestStdlibLogAdapter(t *testing.T) { 22 | t.Run("Simple", func(t *testing.T) { 23 | buf := bytes.Buffer{} 24 | logger := log.NewLogger(log.Options{}) 25 | 26 | logger.SetOutput(&buf) 27 | 28 | InitAdapter(logger) 29 | 30 | stdlog.Print("test string for a check") 31 | stdlog.Print("another string") 32 | 33 | scanner := bufio.NewScanner(bytes.NewReader(buf.Bytes())) 34 | 35 | scanner.Scan() 36 | assertLogLine(t, scanner.Text(), "test string for a check") 37 | 38 | scanner.Scan() 39 | assertLogLine(t, scanner.Text(), "another string") 40 | }) 41 | } 42 | 43 | func assertLogLine(t *testing.T, line string, expected string) { 44 | logLine := testLogLine{} 45 | 46 | fmt.Println(line) 47 | err := json.Unmarshal([]byte(line), &logLine) 48 | require.NoError(t, err) 49 | require.Equal(t, "helm", logLine.Logger) 50 | require.Equal(t, "info", logLine.Level) 51 | require.Contains(t, logLine.Message, expected) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/values/validation/defaulting.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "github.com/go-openapi/spec" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | 7 | "github.com/flant/addon-operator/pkg/utils" 8 | ) 9 | 10 | // ApplyDefaults traverses an object and apply default values from OpenAPI schema. 11 | // It returns true if obj is changed. 12 | // 13 | // See https://github.com/kubernetes/kubernetes/blob/cea1d4e20b4a7886d8ff65f34c6d4f95efcb4742/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting/algorithm.go 14 | // 15 | // Note: check only Properties for object type and List validation for array type. 16 | func ApplyDefaults(obj interface{}, s *spec.Schema) bool { 17 | if s == nil { 18 | return false 19 | } 20 | 21 | res := false 22 | 23 | // Support utils.Values 24 | switch vals := obj.(type) { 25 | case utils.Values: 26 | obj = map[string]interface{}(vals) 27 | case *utils.Values: 28 | // rare case 29 | obj = map[string]interface{}(*vals) 30 | } 31 | 32 | switch obj := obj.(type) { 33 | case map[string]interface{}: 34 | // Apply defaults to properties 35 | for k, prop := range s.Properties { 36 | if prop.Default == nil { 37 | continue 38 | } 39 | if _, found := obj[k]; !found { 40 | obj[k] = runtime.DeepCopyJSONValue(prop.Default) 41 | res = true 42 | } 43 | } 44 | // Apply to deeper levels. 45 | for k, v := range obj { 46 | if prop, found := s.Properties[k]; found { 47 | deepRes := ApplyDefaults(v, &prop) 48 | res = res || deepRes 49 | } 50 | } 51 | case []interface{}: 52 | // If the 'items' section is not specified in the schema, addon-operator will panic here. 53 | // The schema itself should be validated earlier before applying defaults, 54 | // but having a panic in runtime is much bigger problem. 55 | if s.Items == nil { 56 | return res 57 | } 58 | 59 | // Only List validation is supported. 60 | // See https://json-schema.org/understanding-json-schema/reference/array.html#list-validation 61 | for _, v := range obj { 62 | deepRes := ApplyDefaults(v, s.Items.Schema) 63 | res = res || deepRes 64 | } 65 | default: 66 | // scalars, no action 67 | } 68 | 69 | return res 70 | } 71 | -------------------------------------------------------------------------------- /pkg/values/validation/extend_test.go: -------------------------------------------------------------------------------- 1 | package validation_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/gomega" 7 | 8 | "github.com/flant/addon-operator/pkg/module_manager/models/modules" 9 | "github.com/flant/addon-operator/pkg/utils" 10 | ) 11 | 12 | func Test_Validate_Extended(t *testing.T) { 13 | g := NewWithT(t) 14 | 15 | var err error 16 | 17 | configValuesYaml := ` 18 | type: object 19 | additionalProperties: false 20 | required: 21 | - param1 22 | #minProperties: 2 23 | properties: 24 | param1: 25 | type: string 26 | enum: 27 | - val1 28 | param2: 29 | type: string 30 | ` 31 | valuesYaml := ` 32 | x-extend: 33 | schema: "config-values.yaml" 34 | type: object 35 | additionalProperties: false 36 | required: 37 | - memParam 38 | #minProperties: 2 39 | properties: 40 | memParam: 41 | type: string 42 | enum: 43 | - val1 44 | ` 45 | 46 | // TODO: static values 47 | valuesStorage, err := modules.NewValuesStorage("moduleName", nil, []byte(configValuesYaml), []byte(valuesYaml)) 48 | g.Expect(err).ShouldNot(HaveOccurred()) 49 | 50 | var moduleValues utils.Values 51 | 52 | moduleValues, err = utils.NewValuesFromBytes([]byte(` 53 | moduleName: 54 | param1: val1 55 | param2: val2 56 | `)) 57 | g.Expect(err).ShouldNot(HaveOccurred()) 58 | 59 | mErr := valuesStorage.GetSchemaStorage().ValidateValues("moduleName", moduleValues) 60 | 61 | g.Expect(mErr).Should(HaveOccurred()) 62 | 63 | moduleValues, err = utils.NewValuesFromBytes([]byte(` 64 | moduleName: 65 | param1: val1 66 | memParam: val1 67 | `)) 68 | g.Expect(err).ShouldNot(HaveOccurred()) 69 | 70 | mErr = valuesStorage.GetSchemaStorage().ValidateValues("moduleName", moduleValues) 71 | 72 | g.Expect(mErr).ShouldNot(HaveOccurred()) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/values/validation/schema/additional-properties.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "github.com/go-openapi/spec" 4 | 5 | type AdditionalPropertiesTransformer struct { 6 | Parent *spec.Schema 7 | } 8 | 9 | // Transform sets undefined AdditionalProperties to false recursively. 10 | func (t *AdditionalPropertiesTransformer) Transform(s *spec.Schema) *spec.Schema { 11 | if s == nil { 12 | return nil 13 | } 14 | 15 | if s.AdditionalProperties == nil { 16 | s.AdditionalProperties = &spec.SchemaOrBool{ 17 | Allows: false, 18 | } 19 | } 20 | 21 | for k, prop := range s.Properties { 22 | if prop.AdditionalProperties == nil { 23 | prop.AdditionalProperties = &spec.SchemaOrBool{ 24 | Allows: false, 25 | } 26 | ts := prop 27 | s.Properties[k] = *t.Transform(&ts) 28 | } 29 | } 30 | 31 | if s.Items != nil { 32 | if s.Items.Schema != nil { 33 | s.Items.Schema = t.Transform(s.Items.Schema) 34 | } 35 | for i, item := range s.Items.Schemas { 36 | ts := item 37 | s.Items.Schemas[i] = *t.Transform(&ts) 38 | } 39 | } 40 | 41 | return s 42 | } 43 | -------------------------------------------------------------------------------- /pkg/values/validation/schema/copy.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "github.com/go-openapi/spec" 4 | 5 | type CopyTransformer struct{} 6 | 7 | func (t *CopyTransformer) Transform(s *spec.Schema) *spec.Schema { 8 | tmpBytes, _ := s.MarshalJSON() 9 | res := new(spec.Schema) 10 | _ = res.UnmarshalJSON(tmpBytes) 11 | return res 12 | } 13 | -------------------------------------------------------------------------------- /pkg/values/validation/schema/required-for-helm.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/go-openapi/spec" 5 | ) 6 | 7 | type RequiredForHelmTransformer struct{} 8 | 9 | const XRequiredForHelm = "x-required-for-helm" 10 | 11 | func (t *RequiredForHelmTransformer) Transform(s *spec.Schema) *spec.Schema { 12 | if s == nil { 13 | return s 14 | } 15 | 16 | s.Required = MergeRequiredFields(s.Extensions, s.Required) 17 | 18 | // Deep transform. 19 | transformRequired(s.Properties) 20 | return s 21 | } 22 | 23 | func transformRequired(props map[string]spec.Schema) { 24 | for k, prop := range props { 25 | prop.Required = MergeRequiredFields(prop.Extensions, prop.Required) 26 | props[k] = prop 27 | transformRequired(props[k].Properties) 28 | } 29 | } 30 | 31 | func MergeArrays(ar1 []string, ar2 []string) []string { 32 | res := make([]string, 0) 33 | m := make(map[string]struct{}) 34 | for _, item := range ar1 { 35 | res = append(res, item) 36 | m[item] = struct{}{} 37 | } 38 | for _, item := range ar2 { 39 | if _, ok := m[item]; !ok { 40 | res = append(res, item) 41 | } 42 | } 43 | return res 44 | } 45 | 46 | func MergeRequiredFields(ext spec.Extensions, required []string) []string { 47 | var xReqFields []string 48 | _, hasField := ext[XRequiredForHelm] 49 | if !hasField { 50 | return required 51 | } 52 | field, ok := ext.GetString(XRequiredForHelm) 53 | if ok { 54 | xReqFields = []string{field} 55 | } else { 56 | xReqFields, _ = ext.GetStringSlice(XRequiredForHelm) 57 | } 58 | // Merge x-required with required 59 | return MergeArrays(required, xReqFields) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/values/validation/schema/transform.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "github.com/go-openapi/spec" 4 | 5 | type SchemaTransformer interface { 6 | Transform(s *spec.Schema) *spec.Schema 7 | } 8 | 9 | func TransformSchema(s *spec.Schema, transformers ...SchemaTransformer) *spec.Schema { 10 | for _, transformer := range transformers { 11 | s = transformer.Transform(s) 12 | } 13 | return s 14 | } 15 | -------------------------------------------------------------------------------- /pkg/values/validation/testdata/test-schema-bad.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | additionalProperties: false 3 | required: 4 | - project 5 | - clusterName 6 | minProperties: 2 7 | properties: 8 | param1: 9 | type: string 10 | enum: 11 | - val1 12 | param2: 13 | type: ssttrriingg 14 | -------------------------------------------------------------------------------- /pkg/values/validation/testdata/test-schema-ok-project-2.yaml: -------------------------------------------------------------------------------- 1 | # Hide OpenAPI schema inside array to test loading from the $ref-ed fragment. 2 | # Also test local definitions. Note $ref works from the document root, not 3 | # from the fragment root (see test-schema-ok.yaml). 4 | versions: 5 | - version: v1 6 | OpenAPISchema: 7 | definitions: 8 | version: 9 | type: number 10 | description: 11 | type: string 12 | type: object 13 | properties: 14 | project: 15 | type: object 16 | properties: &common_project 17 | name: 18 | type: string 19 | version: 20 | $ref: '#/versions/0/OpenAPISchema/definitions/version' 21 | activeProject: 22 | type: object 23 | properties: *common_project 24 | archive: 25 | type: array 26 | items: 27 | type: object 28 | properties: 29 | <<: *common_project 30 | description: 31 | $ref: '#/versions/0/OpenAPISchema/definitions/description' 32 | -------------------------------------------------------------------------------- /pkg/values/validation/testdata/test-schema-ok-project.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | project: 4 | type: object 5 | properties: &common_project 6 | name: 7 | type: string 8 | version: 9 | type: number 10 | activeProject: 11 | type: object 12 | properties: *common_project 13 | archive: 14 | type: array 15 | items: 16 | type: object 17 | properties: 18 | <<: *common_project 19 | description: 20 | type: string 21 | -------------------------------------------------------------------------------- /pkg/values/validation/testdata/test-schema-ok.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | additionalProperties: false 3 | required: 4 | - clusterName 5 | minProperties: 2 6 | properties: 7 | clusterName: 8 | type: string 9 | clusterHostname: 10 | type: string 11 | project: 12 | type: object 13 | properties: &common_project 14 | name: 15 | type: string 16 | version: 17 | type: number 18 | activeProject: 19 | type: object 20 | properties: *common_project 21 | archive: 22 | type: array 23 | items: 24 | type: object 25 | properties: 26 | <<: *common_project 27 | description: 28 | type: string 29 | externalProjects: 30 | $ref: 'testdata/test-schema-ok-project.yaml' 31 | fragmentedProjects: 32 | $ref: 'testdata/test-schema-ok-project-2.yaml#/versions/0/OpenAPISchema' 33 | -------------------------------------------------------------------------------- /sdk/registry_test.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | 9 | gohook "github.com/flant/addon-operator/pkg/module_manager/go_hook" 10 | "github.com/flant/addon-operator/pkg/module_manager/models/hooks/kind" 11 | ) 12 | 13 | func TestRegister(t *testing.T) { 14 | t.Run("Hook with OnStartup and Kubernetes bindings should panic", func(t *testing.T) { 15 | hook := kind.NewGoHook( 16 | &gohook.HookConfig{ 17 | OnStartup: &gohook.OrderedConfig{Order: 1}, 18 | Kubernetes: []gohook.KubernetesConfig{ 19 | { 20 | Name: "test", 21 | ApiVersion: "v1", 22 | Kind: "Pod", 23 | FilterFunc: nil, 24 | }, 25 | }, 26 | }, 27 | nil, 28 | ) 29 | 30 | defer func() { 31 | r := recover() 32 | require.NotEmpty(t, r) 33 | assert.Equal(t, bindingsPanicMsg, r) 34 | }() 35 | Registry().Add(hook) 36 | }) 37 | 38 | t.Run("Hook with OnStartup should not panic", func(t *testing.T) { 39 | hook := kind.NewGoHook( 40 | &gohook.HookConfig{ 41 | OnStartup: &gohook.OrderedConfig{Order: 1}, 42 | }, 43 | nil, 44 | ) 45 | 46 | defer func() { 47 | r := recover() 48 | assert.NotEqual(t, bindingsPanicMsg, r) 49 | }() 50 | Registry().Add(hook) 51 | }) 52 | 53 | t.Run("Hook with Kubernetes binding should not panic", func(t *testing.T) { 54 | hook := kind.NewGoHook( 55 | &gohook.HookConfig{ 56 | Kubernetes: []gohook.KubernetesConfig{ 57 | { 58 | Name: "test", 59 | ApiVersion: "v1", 60 | Kind: "Pod", 61 | FilterFunc: nil, 62 | }, 63 | }, 64 | }, 65 | nil, 66 | ) 67 | 68 | defer func() { 69 | r := recover() 70 | assert.NotEqual(t, bindingsPanicMsg, r) 71 | }() 72 | Registry().Add(hook) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /sdk/sdk.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | ) 7 | 8 | func ToUnstructured(obj interface{}) (*unstructured.Unstructured, error) { 9 | content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) 10 | return &unstructured.Unstructured{Object: content}, err 11 | } 12 | 13 | func FromUnstructured(unstructuredObj *unstructured.Unstructured, obj interface{}) error { 14 | return runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredObj.UnstructuredContent(), obj) 15 | } 16 | -------------------------------------------------------------------------------- /sdk/test/sdk_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/gomega" 7 | 8 | "github.com/flant/addon-operator/pkg/module_manager/models/hooks/kind" 9 | "github.com/flant/addon-operator/sdk" 10 | _ "github.com/flant/addon-operator/sdk/test/simple_operator/global-hooks" 11 | _ "github.com/flant/addon-operator/sdk/test/simple_operator/modules/001-module-one/hooks" 12 | _ "github.com/flant/addon-operator/sdk/test/simple_operator/modules/002-module-two/hooks/level1/sublevel" 13 | ) 14 | 15 | func Test_HookMetadata_from_runtime(t *testing.T) { 16 | g := NewWithT(t) 17 | 18 | hookList := sdk.Registry().Hooks() 19 | g.Expect(len(hookList)).Should(Equal(3)) 20 | 21 | globalHooks := sdk.Registry().GetGlobalHooks() 22 | g.Expect(len(globalHooks)).Should(Equal(1)) 23 | 24 | hooks := map[string]*kind.GoHook{} 25 | 26 | for _, h := range hookList { 27 | hooks[h.GetName()] = h 28 | } 29 | 30 | hm, ok := hooks["go-hook.go"] 31 | g.Expect(ok).To(BeTrue(), "global go-hook.go should be registered") 32 | g.Expect(hm.GetPath()).To(Equal("/global-hooks/go-hook.go")) 33 | 34 | hm, ok = hooks["001-module-one/hooks/module-one-hook.go"] 35 | g.Expect(ok).To(BeTrue(), "module-one-hook.go should be registered") 36 | g.Expect(hm.GetPath()).To(Equal("/modules/001-module-one/hooks/module-one-hook.go")) 37 | 38 | hm, ok = hooks["002-module-two/hooks/level1/sublevel/sub-sub-hook.go"] 39 | g.Expect(ok).To(BeTrue(), "sub-sub-hook.go should be registered") 40 | g.Expect(hm.GetPath()).To(Equal("/modules/002-module-two/hooks/level1/sublevel/sub-sub-hook.go")) 41 | } 42 | -------------------------------------------------------------------------------- /sdk/test/simple_operator/global-hooks/go-hook.go: -------------------------------------------------------------------------------- 1 | package global_hooks 2 | 3 | import ( 4 | gohook "github.com/flant/addon-operator/pkg/module_manager/go_hook" 5 | "github.com/flant/addon-operator/sdk" 6 | ) 7 | 8 | func init() { 9 | // TODO: remove global logger? 10 | sdk.RegisterFunc(&gohook.HookConfig{}, main) 11 | } 12 | 13 | func main(_ *gohook.HookInput) error { 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /sdk/test/simple_operator/modules/001-module-one/hooks/module-one-hook.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | gohook "github.com/flant/addon-operator/pkg/module_manager/go_hook" 5 | "github.com/flant/addon-operator/sdk" 6 | ) 7 | 8 | // TODO: remove global logger? 9 | var _ = sdk.RegisterFunc(&gohook.HookConfig{}, main) 10 | 11 | func main(_ *gohook.HookInput) error { 12 | return nil 13 | } 14 | -------------------------------------------------------------------------------- /sdk/test/simple_operator/modules/002-module-two/hooks/level1/sublevel/sub-sub-hook.go: -------------------------------------------------------------------------------- 1 | package sublevel 2 | 3 | import ( 4 | gohook "github.com/flant/addon-operator/pkg/module_manager/go_hook" 5 | "github.com/flant/addon-operator/sdk" 6 | ) 7 | 8 | // TODO: remove global logger? 9 | var _ = sdk.RegisterFunc(&gohook.HookConfig{}, main) 10 | 11 | func main(_ *gohook.HookInput) error { 12 | return nil 13 | } 14 | --------------------------------------------------------------------------------