Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 增加了通过 yaml 配置插件 SettingMeta 的能力 #254

Merged
merged 11 commits into from
Jan 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/api/bkuser_core/audit/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from bkuser_core.categories.signals import post_category_create
from bkuser_core.departments.signals import post_department_create
from bkuser_core.profiles.signals import post_field_create, post_profile_create, post_profile_update
from bkuser_core.user_settings.signals import post_setting_create
from bkuser_core.user_settings.signals import post_setting_create, post_setting_update
from django.dispatch import receiver

if TYPE_CHECKING:
Expand Down Expand Up @@ -51,3 +51,14 @@ def create_audit_log(sender, instance: "Profile", operator: str, extra_values: d
operator_obj=instance,
request=extra_values["request"],
)


@receiver([post_setting_update])
def update_audit_log(sender, instance: "Profile", operator: str, extra_values: dict, **kwargs):
"""Create an audit log for instance"""
create_general_log(
operator=operator,
operate_type=OperationType.UPDATE.value,
operator_obj=instance,
request=extra_values["request"],
)
4 changes: 2 additions & 2 deletions src/api/bkuser_core/categories/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ def import_plugins(base_file_path: str, module_prefix: str):
module_path = module_prefix + d
try:
importlib.import_module(module_path)
except Exception as e: # pylint: disable=broad-except
logger.warning("⚠️ failed to import plugin: %s, for %s", module_path, e)
except Exception: # pylint: disable=broad-except
logger.exception("⚠️ failed to import plugin: path[%s]", module_path)
3 changes: 0 additions & 3 deletions src/api/bkuser_core/categories/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@
from django.conf import settings
from django.dispatch import receiver

from .plugins.ldap.handlers import create_sync_tasks, delete_sync_tasks, update_sync_tasks # noqa
from .plugins.local.handlers import make_local_default_settings # noqa

logger = logging.getLogger(__name__)


Expand Down
2 changes: 1 addition & 1 deletion src/api/bkuser_core/categories/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@ def register_plugin(plugin: "DataSourcePlugin"):
logger.warning(f"Plugin with name: {plugin.name} already existed")
except PluginDoesNotExist:
_global_plugins[plugin.name] = plugin
logger.info("➕Plugin[%s] added.", plugin.name)
logger.info("➕Plugin[%s] loaded.", plugin.name)
37 changes: 37 additions & 0 deletions src/api/bkuser_core/categories/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,43 @@ python manage.py create_pluggable_category --name 插件化目录 --domain some-
```
默认地,我们会创建一个 key 为 `plugin_name` 的 `Setting` 绑定到该目录。

## 配置

我们为插件增加了申明配置的能力,开发者可以通过 yaml 文件定义面向使用者的配置列表

```python
DataSourcePlugin(
name="custom",
syncer_cls=CustomSyncer,
login_handler_cls=LoginHandler,
allow_client_write=True,
category_type="custom",
hooks={HookType.POST_SYNC: AlertIfFailedHook},
# 在这里显式地告之插件配置的文件路径
settings_path=os.path.dirname(__file__) / Path("settings.yaml"),
).register()
```
```yaml
# 默认第一层
settings:
IMBlues marked this conversation as resolved.
Show resolved Hide resolved
# namespace
general:
# region
default:
# 具体配置的 key 值
paths:
# SettingMeta 具体内容
default:
# 可以以 yaml 原生写法定义 JSON 内容
profile: "profiles"
department: "departments"
api_host:
default: ""
example: "https://example.com"
```
当插件加载时,我们会做两件事:
- 创建或更新 SettingMeta
- 当 SettingMeta 的默认值存在时(所有不为 `None` 的内容,空字符串、空字典均被视作 **存在**), 使用默认值为所有已经存在对应目录初始化 Setting

## 同步类 Syncer 实现

Expand Down
4 changes: 4 additions & 0 deletions src/api/bkuser_core/categories/plugins/custom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""
import os
from pathlib import Path

from bkuser_core.categories.plugins.plugin import DataSourcePlugin, HookType

from .hooks import AlertIfFailedHook
Expand All @@ -21,4 +24,5 @@
allow_client_write=True,
category_type="custom",
hooks={HookType.POST_SYNC: AlertIfFailedHook},
settings_path=os.path.dirname(__file__) / Path("settings.yaml"),
).register()
7 changes: 7 additions & 0 deletions src/api/bkuser_core/categories/plugins/custom/settings.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
paths:
default:
profile: "profiles"
department: "departments"
api_host:
default: ""
example: "http://example.com"
5 changes: 4 additions & 1 deletion src/api/bkuser_core/categories/plugins/ldap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""
import os
from pathlib import Path

from bkuser_core.categories.plugins.plugin import DataSourcePlugin

from .login import LoginHandler
Expand All @@ -19,5 +22,5 @@
login_handler_cls=LoginHandler,
allow_client_write=False,
category_type="ldap",
extra_config={"default_sync_period": 60, "min_sync_period": 60, "ldap_max_paged_size": 1000},
settings_path=os.path.dirname(__file__) / Path("settings.yaml"),
).register()
3 changes: 1 addition & 2 deletions src/api/bkuser_core/categories/plugins/ldap/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from typing import TYPE_CHECKING, Dict, List

import ldap3
from bkuser_core.categories.loader import get_plugin_by_name
from django.conf import settings
from ldap3 import ALL, SIMPLE, Connection, Server

Expand Down Expand Up @@ -97,7 +96,7 @@ def search(
search_filter=search_filter,
get_operational_attributes=True,
attributes=attributes or [],
paged_size=get_plugin_by_name("ldap").extra_config["ldap_max_paged_size"],
paged_size=self.config_provider.get("ldap_max_paged_size"),
generator=False,
)

Expand Down
65 changes: 32 additions & 33 deletions src/api/bkuser_core/categories/plugins/ldap/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@
from typing import TYPE_CHECKING

from bkuser_core.categories.constants import CategoryType
from bkuser_core.categories.loader import get_plugin_by_category
from bkuser_core.categories.plugins.utils import (
delete_dynamic_filed,
delete_periodic_sync_task,
make_periodic_sync_task,
update_periodic_sync_task,
)
from bkuser_core.categories.signals import post_category_create, post_category_delete, post_dynamic_field_delete
from bkuser_core.categories.signals import post_category_delete, post_dynamic_field_delete
from bkuser_core.user_settings.loader import ConfigProvider
from bkuser_core.user_settings.signals import post_setting_create, post_setting_update
from django.dispatch import receiver

Expand All @@ -31,17 +30,36 @@
logger = logging.getLogger(__name__)


@receiver(post_category_create)
def create_sync_tasks(sender, instance: "ProfileCategory", operator: str, **kwargs):
if instance.type not in [CategoryType.LDAP.value, CategoryType.MAD.value]:
PULL_INTERVAL_SETTING_KEY = "pull_cycle"


def update_or_create_sync_tasks(instance: "Setting", operator: str):
"""尝试创建或更新同步数据任务"""
if not instance.meta.key == PULL_INTERVAL_SETTING_KEY:
return

cycle_value = int(instance.value)
config_provider = ConfigProvider(instance.category_id)

min_sync_period = config_provider.get("min_sync_period")
if cycle_value <= 0:
# 特殊约定,当设置 <= 0 时,删除周期任务
delete_periodic_sync_task(category_id=instance.category_id)
return
# 保证不会用户配置不会低于插件的最低间隔限制
elif cycle_value < min_sync_period:
cycle_value = min_sync_period

logger.info("going to add periodic task for Category<%s>", instance.id)
make_periodic_sync_task(
category_id=instance.id,
operator=operator,
interval_seconds=get_plugin_by_category(instance).extra_config["default_sync_period"],
# 尝试更新周期任务周期
logger.info(
"going to update category<%s> sync interval to %s",
instance.category_id,
cycle_value,
)
try:
update_periodic_sync_task(category_id=instance.category_id, operator=operator, interval_seconds=cycle_value)
except Exception: # pylint: disable=broad-except
logger.exception("failed to update periodic task schedule")


@receiver(post_category_delete)
Expand All @@ -59,31 +77,12 @@ def update_sync_tasks(sender, instance: "Setting", operator: str, **kwargs):
if instance.category.type not in [CategoryType.LDAP.value, CategoryType.MAD.value]:
return

if not instance.meta.key == "pull_cycle":
return

cycle_value = int(instance.value)
category_config = get_plugin_by_category(instance.category)
if cycle_value <= 0:
delete_periodic_sync_task(category_id=instance.category_id)
return

elif cycle_value < category_config.extra_config["min_sync_period"]:
cycle_value = category_config.extra_config["min_sync_period"]

# 尝试更新周期任务周期
logger.info(
"going to update category<%s> sync interval to %s",
instance.category_id,
cycle_value,
)
try:
update_periodic_sync_task(category_id=instance.category_id, operator=operator, interval_seconds=cycle_value)
except Exception: # pylint: disable=broad-except
logger.exception("failed to update periodic task schedule")
# 针对 pull_cycle 配置更新同步任务
update_or_create_sync_tasks(instance, operator)


@receiver(post_dynamic_field_delete)
def update_dynamic_field_mapping(sender, instance: "DynamicFieldInfo", **kwargs):
"""尝试刷新自定义字段映射配置"""
delete_dynamic_filed(dynamic_field=instance.name)
logger.info("going to delete <%s> from dynamic_field_mapping", instance.name)
122 changes: 122 additions & 0 deletions src/api/bkuser_core/categories/plugins/ldap/settings.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
ldap_max_paged_size:
default: 1000
min_sync_period:
default: 60
default_sync_period:
default: 120

user_member_of:
default: memberOf
example: memberOf
region: group
namespace: fields
user_group_description:
default: description
example: description
required: true
region: group
namespace: fields
user_group_name:
default: cn
example: cn
required: true
region: group
namespace: fields
user_group_filter:
default: (objectclass=groupOfUniqueNames)
example: (objectclass=groupOfUniqueNames)
required: true
region: group
namespace: fields
user_group_class:
default: groupOfUniqueNames
example: groupOfUniqueNames
required: true
region: group
namespace: fields

telephone:
default: telephonenumber
example: telephonenumber
required: true
region: basic
namespace: fields
email:
default: email
example: email
required: true
region: basic
namespace: fields
display_name:
default: displayName
example: displayName
required: true
region: basic
namespace: fields
username:
default: cn
required: true
region: basic
namespace: fields
organization_class:
default: organizationalUnit
example: organizationalUnit
required: true
region: basic
namespace: fields
user_filter:
default: (objectclass=inetorgperson)
example: (objectclass=inetorgperson)
required: true
region: basic
namespace: fields
user_class:
default: inetorgperson
example: inetorgperson
required: true
region: basic
namespace: fields
basic_pull_node:
required: true
region: basic
namespace: fields

dynamic_fields_mapping:
required: false
default: {}
region: extend
namespace: fields

password:
example: password
required: true
namespace: connection
user:
example: username
required: true
namespace: connection
base_dn:
example: CN
required: true
namespace: connection
pull_cycle:
default: 60
example: 60
required: true
namespace: connection
timeout_setting:
default: 120
example: 120
required: true
namespace: connection
ssl_encryption:
choices:
- "\u65E0"
- SSL
default: "\u65E0"
required: true
namespace: connection
connection_url:
example: ldap://localhost:389
required: true
namespace: connection
2 changes: 2 additions & 0 deletions src/api/bkuser_core/categories/plugins/local/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from .login import LoginHandler
from .syncer import ExcelSyncer

# Q: 为什么 local 插件不使用 PluginConfig 注册 SettingMeta ?
# A: 因为目前与 local 插件相关的大部分配置在整个登录流程中都有使用,相当于全局配置,所以暂不放在插件配置中
DataSourcePlugin(
name="local",
syncer_cls=ExcelSyncer,
Expand Down
Loading