mirror of
https://github.com/halo-dev/docs.git
synced 2025-10-20 17:54:01 +00:00
feat: add plugin development docs (#123)
### What this PR does? 添加插件开发文档 ### Which issue(s) this PR fixes Fixes #113 /area docs ```release-note None ```
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: 与自定义模型交互
|
||||
description: 了解如果通过代码的方式操作数据
|
||||
---
|
||||
|
||||
Halo 提供了两个类用于与自定义模型数据交互 `ExtensionClient` 和 `ReactiveExtensionClient`。
|
||||
|
||||
它们的本质就是操作数据库,区别在于 `ExtensionClient` 是阻塞式 API,而 `ReactiveExtensionClient` 是响应式 API,接口返回值只有两种 Mono 或 Flux,它们由 [reactor](https://projectreactor.io/) 提供。
|
||||
|
||||
```java
|
||||
public interface ReactiveExtensionClient {
|
||||
|
||||
/**
|
||||
* Lists Extensions by Extension type, filter and sorter.
|
||||
*
|
||||
* @param type is the class type of Extension.
|
||||
* @param predicate filters the reEnqueue.
|
||||
* @param comparator sorts the reEnqueue.
|
||||
* @param <E> is Extension type.
|
||||
* @return all filtered and sorted Extensions.
|
||||
*/
|
||||
<E extends Extension> Flux<E> list(Class<E> type, Predicate<E> predicate,
|
||||
Comparator<E> comparator);
|
||||
|
||||
/**
|
||||
* Lists Extensions by Extension type, filter, sorter and page info.
|
||||
*
|
||||
* @param type is the class type of Extension.
|
||||
* @param predicate filters the reEnqueue.
|
||||
* @param comparator sorts the reEnqueue.
|
||||
* @param page is page number which starts from 0.
|
||||
* @param size is page size.
|
||||
* @param <E> is Extension type.
|
||||
* @return a list of Extensions.
|
||||
*/
|
||||
<E extends Extension> Mono<ListResult<E>> list(Class<E> type, Predicate<E> predicate,
|
||||
Comparator<E> comparator, int page, int size);
|
||||
|
||||
/**
|
||||
* Fetches Extension by its type and name.
|
||||
*
|
||||
* @param type is Extension type.
|
||||
* @param name is Extension name.
|
||||
* @param <E> is Extension type.
|
||||
* @return an optional Extension.
|
||||
*/
|
||||
<E extends Extension> Mono<E> fetch(Class<E> type, String name);
|
||||
|
||||
Mono<Unstructured> fetch(GroupVersionKind gvk, String name);
|
||||
|
||||
<E extends Extension> Mono<E> get(Class<E> type, String name);
|
||||
|
||||
/**
|
||||
* Creates an Extension.
|
||||
*
|
||||
* @param extension is fresh Extension to be created. Please make sure the Extension name does
|
||||
* not exist.
|
||||
* @param <E> is Extension type.
|
||||
*/
|
||||
<E extends Extension> Mono<E> create(E extension);
|
||||
|
||||
/**
|
||||
* Updates an Extension.
|
||||
*
|
||||
* @param extension is an Extension to be updated. Please make sure the resource version is
|
||||
* latest.
|
||||
* @param <E> is Extension type.
|
||||
*/
|
||||
<E extends Extension> Mono<E> update(E extension);
|
||||
|
||||
/**
|
||||
* Deletes an Extension.
|
||||
*
|
||||
* @param extension is an Extension to be deleted. Please make sure the resource version is
|
||||
* latest.
|
||||
* @param <E> is Extension type.
|
||||
*/
|
||||
<E extends Extension> Mono<E> delete(E extension);
|
||||
}
|
||||
```
|
||||
|
||||
### 示例
|
||||
|
||||
如果你想在插件中根据 name 参数查询获取到 Person 自定义模型的数据,则可以这样写:
|
||||
|
||||
```java
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
Mono<Person> getPerson(String name) {
|
||||
return client.fetch(Person.class, name);
|
||||
}
|
||||
```
|
||||
|
||||
或者使用阻塞式 API:
|
||||
|
||||
```java
|
||||
private final ExtensionClient client;
|
||||
|
||||
Optional<Person> getPerson(String name) {
|
||||
return client.fetch(Person.class, name);
|
||||
}
|
||||
```
|
||||
|
||||
我们建议你更多的使用响应式的 `ReactiveExtensionClient` 去替代 `ExtensionClient`。
|
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: 自定义模型
|
||||
description: 了解什么是自定义模型及如何创建
|
||||
---
|
||||
|
||||
Halo 自定义模型主要参考自 [Kubernetes CRD](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/) 。自定义模型遵循 [OpenAPI v3](https://spec.openapis.org/oas/v3.1.0)。设计目的在于为插件提供自定义数据支持。比如某插件需要存储自定义数据,同时也想读取和操作自定义数据。
|
||||
|
||||
一个典型的自定义模型 `Java` 代码示例如下:
|
||||
|
||||
```java
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
import run.halo.app.extension.GroupKind;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@GVK(group = "my-plugin.halo.run",
|
||||
version = "v1alpha1",
|
||||
kind = "Person",
|
||||
plural = "persons",
|
||||
singular = "person")
|
||||
public class Person extends AbstractExtension {
|
||||
|
||||
@Schema(description = "The description on name field", maxLength = 100)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "The description on age field", maximum = "150", minimum = "0")
|
||||
private Integer age;
|
||||
|
||||
@Schema(description = "The description on gender field")
|
||||
private Gender gender;
|
||||
|
||||
private Person otherPerson;
|
||||
|
||||
public enum Gender {
|
||||
MALE, FEMALE,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
要创建一个自定义模型需要三步:
|
||||
|
||||
1. 创建一个类继承 `AbstractExtension`。
|
||||
2. 使用 `GVK` 注解。
|
||||
3. 在插件 `start()` 生命周期方法中注册自定义模型:
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private SchemeManager schemeManager;
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
schemeManager.register(Person.class);
|
||||
}
|
||||
```
|
||||
|
||||
有了自定义模型后可以通过在插件项目的 `src/main/resources/extensions` 目录下声明 `yaml` 文件来创建一个实例,此目录下的所有自定义模型 `yaml` 都会在插件启动时被创建:
|
||||
|
||||
```yaml
|
||||
groupVersion: my-plugin.halo.run/v1alpha1
|
||||
kind: Person
|
||||
metadata:
|
||||
name: fake-person
|
||||
name: halo
|
||||
age: 18
|
||||
gender: male
|
||||
```
|
||||
|
||||
:::tip 释义
|
||||
|
||||
- @GVK:此注解标识该类为一个自定义模型,同时必须继承 `AbstractExtension`。
|
||||
- kind:表示自定义模型所表示的 REST 资源。
|
||||
- group:表示一组公开的资源,通常采用域名形式,Halo 项目保留使用空组和任何以“*.halo.run”结尾的组名供其单独使用。
|
||||
选择群组名称时,我们建议选择你的群组或组织拥有的子域,例如“widget.mycompany.com”。
|
||||
- version:API 的版本,它与 group 组合使用为 apiVersion=“GROUP/VERSION”,例如“api.halo.run/v1alpha1”。
|
||||
- singular: 资源的单数名称,这允许客户端不透明地处理复数和单数,必须全部小写。通常为小写的“kind”。
|
||||
- plural: 资源的复数名称,自定义资源在 `/apis/<group>/<version>/.../<plural>` 下提供,必须为全部小写。
|
||||
- @Schema:属性校验注解,会在创建/修改资源前对资源校验,参考 [schema-validator](https://www.openapi4j.org/schema-validator.html)。
|
||||
:::
|
||||
|
||||
### 自定义模型 API
|
||||
|
||||
定义好自定义模型并注册后,会根据 `GVK` 注解自动生成一组 `CRUD` API,规则为:
|
||||
`/apis/<group>/<version>/<extension>/{extensionname}/<subextension>`
|
||||
|
||||
对于上述 Person 自定义模型将有以下 APIs:
|
||||
|
||||
```text
|
||||
GET /apis/my-plugin.halo.run/v1alpha1/persons
|
||||
PUT /apis/my-plugin.halo.run/v1alpha1/persons/{name}
|
||||
POST /apis/my-plugin.halo.run/v1alpha1/persons
|
||||
DELETE /apis/my-plugin.halo.run/v1alpha1/persons/{name}
|
||||
```
|
||||
|
||||
### 自定义 API
|
||||
|
||||
在一些场景下,只有自动生成的 `CRUD` API 往往是不够用的,此时可以通过自定义一些 API 来满足功能。
|
||||
|
||||
你可以使用 `SpringBoot` 的控制器写法来暴露 API,不同的是需要添加 `@ApiVersion` 注解,没有此注解的 `Controller` 将被忽略:
|
||||
|
||||
```java
|
||||
@ApiVersion("v1alpha1")
|
||||
@RequestMapping("/apples")
|
||||
@RestController
|
||||
public class AppleController {
|
||||
|
||||
@PostMapping("/starting")
|
||||
public void starting() {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当插件被启动时,Halo 将会为此 AppleController 生成统一路径的 API。API 前缀组成规则如下:
|
||||
|
||||
```text
|
||||
/apis/plugin.api.halo.run/{version}/plugins/{plugin-name}/**
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```text
|
||||
/apis/plugin.api.halo.run/v1alpha1/plugins/my-plugin/apples/starting
|
||||
```
|
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: 静态资源代理
|
||||
description: 了解如果使用静态资源代理来访问插件中的静态资源
|
||||
---
|
||||
|
||||
插件中的静态资源如图片等如果想被外部访问到,需要放到 `src/main/resources` 目录下,并通过创建 ReverseProxy 自定义模型来进行静态资源代理访问。
|
||||
|
||||
例如 `src/main/resources` 下的 `static` 目录下有一张 `halo.jpg`:
|
||||
|
||||
1. 首先需要在 `src/main/resources/extensions` 下创建一个 `yaml`,文件名可以任意。
|
||||
2. 示例配置如下。
|
||||
|
||||
```yaml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ReverseProxy
|
||||
metadata:
|
||||
# name 为此资源的唯一标识名称,不允许重复,为了避免与其他插件冲突,推荐带上插件名称前缀
|
||||
name: my-plugin-fake-reverse-proxy
|
||||
rules:
|
||||
- path: /res/**
|
||||
file:
|
||||
directory: static
|
||||
# 如果想代理 static 下所有静态资源则省略 filename 配置
|
||||
filename: halo.jpg
|
||||
```
|
||||
|
||||
插件启动后会根据 `/plugins/{plugin-name}/assets/**` 规则生成 API。
|
||||
因此该 `ReverseProxy` 的访问路径为: `/plugins/my-plugin/assets/res/halo.jpg`。
|
||||
|
||||
- `rules` 下可以添加多组规则。
|
||||
- `path` 为路径前缀。
|
||||
- `file` 表示访问文件系统,目前暂时仅支持这一种。
|
||||
- `directory` 表示要代理的目标文件目录,它相对于 `src/main/resources/` 目录。
|
||||
- `filename` 表示要代理的目标文件名。
|
||||
|
||||
`directory` 和 `filename` 都是可选的,但必须至少有一个被配置。
|
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: API 权限控制
|
||||
description: 了解如果对插件中的 API 定义角色模板以接入权限控制
|
||||
---
|
||||
|
||||
插件中的 APIs 无论是自定义模型自动生成的 APIs 或者是通过 Controller 自定义的 APIs 都只有超级管理员能够访问,如果想将这些 APIs 授权给其他用户访问则需要定义一些 RoleTemplate 的资源以便可以在用户界面上将其分配给其他角色使用。
|
||||
|
||||
RoleTemplate 的 yaml 资源也需要放到 `src/main/resources/extensions` 目录下,文件名称可以任意,它的 kind 为 Role 但需要一个 label `halo.run/role-template: "true"` 来表示它是角色模板。
|
||||
|
||||
Halo 的权限控制对同一种资源一般只定义两个 RoleTemplate,一个是只读权限,另一个是管理权限,因此如果没有特殊情况需要更细粒度的控制,我们建议你也保持一致:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Role
|
||||
metadata:
|
||||
# 使用 plugin name 作为前缀防止与其他插件冲突
|
||||
name: my-plugin-role-view-persons
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/module: "Persons Management"
|
||||
rbac.authorization.halo.run/display-name: "Person Manage"
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
["plugin:my-plugin:person:view"]
|
||||
rules:
|
||||
- apiGroups: ["my-plugin.halo.run"]
|
||||
resources: ["my-plugin/persons"]
|
||||
verbs: ["*"]
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: my-plugin-role-manage-persons
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/dependencies: |
|
||||
[ "role-template-view-person" ]
|
||||
rbac.authorization.halo.run/module: "Persons Management"
|
||||
rbac.authorization.halo.run/display-name: "Person Manage"
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
["plugin:my-plugin:person:manage"]
|
||||
rules:
|
||||
- apiGroups: [ "my-plugin.halo.run" ]
|
||||
resources: [ "my-plugin/persons" ]
|
||||
verbs: [ "get", "list" ]
|
||||
```
|
||||
|
||||
上述便是根据 [自定义模型](./extension.md) 章节中定义的 Person 自定义模型配置角色模板的示例。
|
||||
|
||||
定义了一个用于管理 Person 资源的角色模板 `my-plugin-role-manage-persons`,它具有所有权限,同时定义了一个只允许查询 Person 资源的角色模板 `my-plugin-role-view-persons`。
|
||||
|
||||
下面让我们回顾一下这些配置:
|
||||
|
||||
`rules` 是个数组,它允许配置多组规则:
|
||||
|
||||
- `apiGroups` 对应 `GVK` 中的 `group` 所声明的值。
|
||||
- `resources` 对应 API 中的 resource 部分,`my-plugin` 表示插件名称。
|
||||
- `verbs` 表示请求动词,可选值为 "create", "delete", "deletecollection", "get", "list", "patch", "update"。对应的 HTTP 请求方式如下表所示:
|
||||
|
||||
| HTTP verb | request verb |
|
||||
| --------- | ------------------------------------------------------------ |
|
||||
| POST | create |
|
||||
| GET, HEAD | get (for individual resources), list (for collections, including full object content), watch (for watching an individual resource or collection of resources) |
|
||||
| PUT | update |
|
||||
| PATCH | patch |
|
||||
| DELETE | delete (for individual resources), deletecollection (for collections) |
|
||||
|
||||
`metadata.labels` 中必须包含 `halo.run/role-template: "true"` 以表示它此资源要作为角色模板。
|
||||
|
||||
`metadata.annotations` 中:
|
||||
|
||||
- `rbac.authorization.halo.run/dependencies`:用于声明角色间的依赖关系,例如管理角色必须要依赖查看角色,以避免分配了管理权限却没有查看权限的情况。
|
||||
- `rbac.authorization.halo.run/module`:角色模板分组名称。在此示例中,管理 Person 的模板角色将和查看 Person 的模板角色将被在 UI 层面归为一组展示。
|
||||
- `rbac.authorization.halo.run/display-name`:模板角色的显示名称,用于展示为用户可读的名称信息。
|
||||
- `rbac.authorization.halo.run/ui-permissions`:用于控制 UI 权限,规则为 `plugin:{your-plugin-name}:scope-name`,使用示例为在插件前端部分入口文件 `index.ts` 中用于控制菜单是否显示或者控制页面按钮是否展示:
|
||||
|
||||
```javascript
|
||||
{
|
||||
path: "",
|
||||
name: "HelloWorld",
|
||||
component: DefaultView,
|
||||
meta: {
|
||||
permissions: ["plugin:my-plugin:person:view"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
以上定义角色模板的方式适合资源型请求,即需要符合以下规则
|
||||
|
||||
- 以 `/api/<version>/<resource>[/<resourceName>/<subresource>/<subresourceName>]` 规则组成 APIs。
|
||||
- 以 `/apis/<group>/<version>/<resource>[/<resourceName>/<subresource>/<subresourceName>]` 规则组成的 APIs。
|
||||
|
||||
注:`[]`包裹的部分表示可选
|
||||
|
||||
如果你的 API 不符合以上资源型 API 的规则,则被定型为非资源型 API,例如 `/healthz`,需要使用一下配置方式:
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
# nonResourceURL 中的 '*' 是一个全局通配符
|
||||
- nonResourceURLs: ["/healthz", "/healthz/*"]
|
||||
verbs: [ "get", "create"]
|
||||
```
|
Reference in New Issue
Block a user