第41节 速读 horizon 源码,核心设计思想


❤️💕💕记录sealosopen in new window开源项目的学习过程。k8s,docker和云原生的学习open in new window。Myblog:http://nsddd.topopen in new window


[TOC]

开始

由于 horzon 项目理解的并没有 sealer、sealos 深入,算是一个从零开始学习的项目。

准备速过一遍 horzon 的源码,以及部分的细节,先看目录结构,分析现在的基本功能,再观察第一个 PR,以及最开始的 Important issue

目录结构

❯ tree -L 2
.
├── CODE-OF-CONDUCT.md
├── CONTRIBUTING.md
├── Horizon.svg
├── LICENSE
├── Makefile
├── README.md
├── README_ZH-CN.md
├── bin
│   └── app
├── build
│   ├── build.sh
│   ├── core
│   └── swagger
├── build-json-schema.json
├── build-ui-schema.json
├── config.yaml
├── core
│   ├── cmd
│   ├── common
│   ├── config
│   ├── controller
│   ├── errors
│   ├── http
│   ├── main.go
│   └── middleware
├── db
│   ├── 20210908_initial_schema.sql
│   ├── 20211124.sql
│   ├── 20211125.sql
│   ├── 20220113.sql
│   ├── 20220124.sql
│   ├── 20220330.sql
│   ├── 20220516.sql
│   ├── 20220519.sql
│   ├── 20220712.sql
│   ├── 20220722.sql
│   ├── 20220816.sql
│   ├── 20220823.sql
│   ├── 20220824.sql
│   ├── 20220908.sql
│   ├── 20220920.sql
│   ├── 20220921.sql
│   ├── 20221031.sql
│   ├── 20221110.sql
│   ├── 20221111.sql
│   ├── 20221117.sql
│   ├── 20221121.sql
│   ├── 20221201.sql
│   ├── 20230302.sql
│   └── migrations
├── go.mod
├── go.sum
├── image
│   └── readme
├── integrationtest
│   └── authpagerender_test.go
├── lib
│   ├── gitlab
│   ├── orm
│   ├── q
│   └── s3
├── mock
│   ├── lib
│   └── pkg
├── openapi
│   ├── v1
│   └── v2
├── pkg
│   ├── accesstoken
│   ├── application
│   ├── applicationregion
│   ├── auth
│   ├── authentication
│   ├── cluster
│   ├── common
│   ├── config
│   ├── context
│   ├── environment
│   ├── environmentregion
│   ├── errors
│   ├── event
│   ├── eventhandler
│   ├── git
│   ├── grafana
│   ├── group
│   ├── hook
│   ├── horizonapp
│   ├── idp
│   ├── jobs
│   ├── member
│   ├── oauth
│   ├── param
│   ├── pipelinerun
│   ├── rbac
│   ├── region
│   ├── registry
│   ├── server
│   ├── tag
│   ├── template
│   ├── templaterelease
│   ├── templaterepo
│   ├── templateschematag
│   ├── token
│   ├── user
│   ├── userlink
│   ├── util
│   └── webhook
├── roles.yaml
├── scopes.yaml
└── scripts
    ├── install.sh
    ├── k3s
    └── make-rules

注意 k3s 和 make-rules 还没有合并进去,我们📜 对上面的解释:

  • CODE-OF-CONDUCT.md:行为守则。
  • CONTRIBUTING.md:贡献指南。
  • Horizon.svg:项目图标。
  • LICENSE:开源许可证。
  • Makefile:构建脚本。
  • README.md:项目说明文档。
  • README_ZH-CN.md:中文说明文档。
  • bin/app:二进制文件。
  • build/build.sh:构建脚本。
  • build/core:构建核心。
  • build/swagger:构建 swagger 文档。
  • build-json-schema.json:JSON 架构文件。
  • build-ui-schema.json:UI 架构文件。
  • config.yaml:配置文件。
  • core:核心代码。
  • db:数据库脚本。
  • go.modgo.sum:依赖管理文件。
  • image/readme:镜像说明文档。
  • integrationtest/authpagerender_test.go:集成测试代码。
  • lib/gitlab:GitLab 相关代码。
  • lib/orm:ORM 相关代码。
  • lib/q:查询相关代码。
  • lib/s3:S3 相关代码。
  • mock:模拟代码。
  • openapi:OpenAPI 相关代码。
  • pkg:公共包。
  • roles.yaml:角色配置文件。
  • scopes.yaml:作用域配置文件。
  • scripts:脚本文件。

Maybe it can be fix:

  • build 迁移到 scripts 中,Dockerfile 迁移出来
  • core 目录改成 internal 目录

第一版

很可惜的是 horizon 并没有 0.1 版本的,而是从 2.0.1 开始开源的:

v2.0.1
v2.0.10
v2.0.11
v2.0.12
v2.0.2
v2.0.2-rc1
v2.0.3
v2.0.4
v2.0.5
v2.0.6
v2.0.7
v2.0.8
v2.0.9

这样看着太不友好了,同样对于 分支的 设定:

dependabot/go_modules/github.com/prometheus/client_golang-1.11.1
dependabot/go_modules/golang.org/x/net-0.7.0
dependabot/go_modules/helm.sh/helm/v3-3.11.1
kilosonc-patch-1
kilosonc-patch-2
kilosonc-patch-3
main

项目中也没用 CHANGELOG 来描述版本信息的变更,这里可以改变下,好有 CHANGELOG 用来展示每个版本之间的变更内容,作为 Release Note 的一部分。但是,如果每次都要手动编写 CHANGELOG,会很麻烦,也不容易坚持,所以这里我们可以借助git-chglogopen in new windowopen in new window工具来自动生成。

不仅如此,一个项目也需要有一个版本号,当前用得比较多的是语义化版本号规范。但如果靠开发者手动打版本号,工作效率低不说,经常还会出现漏打、打的版本号不规范等问题。所以最好的办法是,版本号也通过工具自动生成。在这里我们可以采用gsemveropen in new windowopen in new window工具来自动生成版本号。

我们可以将其放入到 scripts/ensure_tag.sh 脚本中,可以通过 Makefile 和 CI 流程进行调用。

除此之外,对于一个优质的开源项目来说,分为开发阶段和测试解决,这些都是可以用 CICD 流程进行控制的。在开发阶段生成代码、版权检查、代码格式化、静态代码检查、单元测试、构建等操作都是可以应用到 Makefile 和 CICD 流的。

我们 阅读下 horizon 最早的版本(由于并没有对应合适的 branch 和 tag,我们选择 Frist commit):

image-20230416104617735

第一个版本的提交是对 openAPI 的规范

OpenAPI 是一种规范,用于定义 RESTful API,它基于 JSON 或 YAML 格式,用于描述 API 的输入参数、输出结果等详细信息,以便于生成客户端 SDK 和 API 文档。在 Horizon 项目中,openapi 文件夹存放着 OpenAPI 相关的代码。

openapi: 3.0.0
info:
  title: Horizon Group API
  description: |
    This API
  version: 1.0.0
tags:
  - name: group
paths:
  /groups:
    get:
      operationId: listGroups
      summary: list groups
      parameters:
        - $ref: "#/components/parameters/searchParam"
      responses:
        "201":
          description: null response
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/error"
  /group:
    post:
      operationId: createGroup
      summary: create a group
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/newGroup"
      responses:
        "201":
          description: null response
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/error"
  /group/{group}:
    parameters:
      - $ref: "#/components/parameters/groupParam"
    get:
      operationId: getGroupDetail
      summary: get the detail of a group
      responses:
        '200':
          description: Expected response to a valid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/groupDetail"
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/error"
    put:
      operationId: updateGroupDetail
      summary: update detail of a group
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/groupDetail'
      responses:
        '200':
          description: Update result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/groupDetail'
        default:
          description:  Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/error'
    delete:
      operationId: deleteGroup
      summary: delete an empty group
      responses:
        '200':
          description: null response
        default:
          description:  Unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/error'
  /group/{group}/subgroups:
    parameters:
      - $ref: "#/components/parameters/groupParam"
      - $ref: "#/components/parameters/searchParam"
    get:
      operationId: listSubGroups
      responses:
        '200':
          description: Return the list of groups
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/groupDetail'
  /group/{group}/services:
    parameters:
      - $ref: "#/components/parameters/groupParam"
      - $ref: "#/components/parameters/searchParam"
    get:
      operationId: listGroupService
      responses:
        '200':
          description: Return the list of service
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: 'service.yaml#/components/schemas/serviceDetail'
components:
  parameters:
    groupParam:
      name: group
      in: path
      required: true
      schema:
        type: string
    searchParam:
      name: search
      in: query
      description: Return the list of authorized groups matching the search criteria
      required: false
      schema:
        type: string
  schemas:
    queryGroup:
      type: string
      maxLength: 100
      description: param for querying group
    newGroup:
      type: object
      required:
        - name
        - path
        - visibilityLevel
      properties:
        name:
          $ref: '#/components/schemas/groupName'
        path:
          $ref: '#/components/schemas/groupPath'
        description:
          $ref: '#/components/schemas/groupDescription'
        visibilityLevel:
          $ref: '#/components/schemas/visibilityLevel'
        parantId:
          $ref: '#/components/schemas/groupId'

    groupDetail:
      type: object
      properties:
        id:
          $ref: '#/components/schemas/groupId'
        name:
          $ref: '#/components/schemas/groupName'
        path:
          $ref: '#/components/schemas/groupPath'
        description:
          $ref: '#/components/schemas/groupDescription'
        visibilityLevel:
          $ref: '#/components/schemas/visibilityLevel'
        createTime:
          $ref: '#/components/schemas/date'
    date:
      type: string
      format: date
      pattern: full-date
    groupId:
      type: integer
      format: int64
      description: the parent id of the subgroup,if not provided. a root group
    groupName:
      type: string
      maxLength: 64
      description: the group Name
    groupPath:
      type: string
      maxLength: 1024
      description: the group path
    visibilityLevel:
      type: integer
      format: int32
      enum: [0,1]
      default: 0
      description: 0 means public(every one can see the group), 1 means private one member can see
    groupDescription:
      type: string
      maxLength: 1024
      description: the group description
    error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: integer
          format: int32
        message:
          type: string

Swagger 和 OpenAPI 的区别:

优化: 用 go-swaggeropen in new window 来生成 Swagger API 文档

为什么不用 swag?

  • go-swagger 比 swag 功能更强大: go-swagger 提供了更灵活、更多的功能来描述我们的 API。
  • 使我们的代码更易读:如果使用 swag,我们每一个 API 都需要有一个冗长的注释,有时候代码注释比代码还要长,但是通过 go-swagger 我们可以将代码和注释分开编写,一方面可以使我们的代码保持简洁,清晰易读,另一方面我们可以在另外一个包中,统一管理这些 Swagger API 文档定义。
  • 更好的社区支持:go-swagger 目前有非常多的 Github star 数,出现 Bug 的概率很小,并且处在一个频繁更新的活跃状态。

nocalhost

nocalhost 是一种基于 Kubernetes 的本地开发平台,它可以帮助开发者在本地快速构建和测试应用程序。使用 nocalhost,你可以轻松地在本地运行完整的 Kubernetes 环境,并在其中部署和测试应用程序。以下是 nocalhost 的主要功能:

  • 通过 nocalhost,你可以在本地启动一个完整的 Kubernetes 环境,包括所有必要的组件和插件。
  • nocalhost 支持各种不同类型的应用程序,包括 Java、Node.js、Python 等。
  • 你可以使用 nocalhost 在本地构建和运行应用程序,以便于开发和测试。
  • nocalhost 还提供了一些有用的工具和插件,例如调试器、日志查看器、数据库管理器等。
  • nocalhost 还提供了完整的文档和教程,以便于开发者快速上手。

main 入口

从 main.go 函数开始阅读:

func main() {
	cmd.Run(cmd.ParseFlags())
}

从 Run 函数开始实现逻辑:

// Run runs the agent.
func Run(flags *Flags) {
	ctx, cancelFunc := context.WithCancel(context.Background())
	setTasksBeforeExit(cancelFunc)
	configs, err := LoadConfig(flags)
	if err != nil {
		panic(err)
	}
	// enable pprof
	runPProfServer(&configs.PProf)
	// init api
	Init(ctx, flags, configs)
}

Run 函数实现的逻辑很简单:

  • 接受一个 flags 参数,它包含了代理运行的一些配置信息。
  • 创建了一个上下文(ctx)和一个取消函数(cancelFunc)。这些变量用于在代理运行过程中控制程序的行为。
  • 调用了 LoadConfig 函数,该函数用于加载代理的配置信息。如果代理配置信息加载失败,则会抛出一个异常。
  • 调用了 runPProfServer 函数,该函数用于启动 pprof 服务器,以便于进行性能调试和优化。
  • 最后,Run 函数调用了 Init 函数,该函数用于初始化代理并启动 API 服务。

查看 Init() 函数的实现逻辑:

初始化逻辑有些复杂,实现的部分应该尽可能的放在 pkg 或者 internal 目录下面。

大致的步骤如下:

  1. 初始化群组服务、管理器
  2. 初始化集群服务、用户服务、令牌服务、Grafana 服务等
  3. 创建一个参数对象,该对象包含了所有服务和服务所需的参数
  4. 初始化控制器,例如成员控制器、应用程序控制器、环境模板控制器等
  5. 初始化 API,包括 v1 API 和 v2 API,并将它们注册到路由中
  6. 启动云事件服务器,以便在云中处理事件
  7. 启动 API 服务器并监听端口

使用 gorm 作为回调函数:

func RegisterCustomCallbacks(db *gorm.DB) {
	_ = db.Callback().Create().After("gorm:before_create").Before("gorm:create").
		Register("add_created_by", addCreatedByUpdatedByForCreateCallback)

	_ = db.Callback().Update().After("gorm:before_update").Before("gorm:update").
		Register("add_updated_by", addUpdatedByForUpdateDeleteCallback)

	_ = db.Callback().Delete().After("gorm:before_delete").Before("gorm:delete").
		Register("add_updated_by", addUpdatedByForUpdateDeleteCallback)
}

主要是在执行数据库操作前后执行一些自定义的逻辑。它包含了三个回调函数:CreateUpdateDelete。每个回调函数都在 GORM 的某个操作之前或之后被调用,并在回调函数中执行一些自定义的逻辑。例如,回调函数 "add_created_by" 和 "add_updated_by" 用于在创建或更新数据时添加一个 "created_by" 或 "updated_by" 字段,用于记录数据的创建或更新者。其中,addCreatedByUpdatedByForCreateCallbackaddUpdatedByForUpdateDeleteCallback 是两个自定义函数,它们分别用于创建和更新/删除操作中的回调逻辑。该函数还包括了一些 GORM 的回调注册逻辑,用于将自定义的回调函数注册到 GORM 中,以便在执行数据库操作时自动调用这些回调函数。

dao

和 Java 的风格类似, horizon 中有很多都是 dao 目录,dao目录是一种常见的命名规范,代表数据访问对象(Data Access Object)。它通常用于存储与数据库交互的代码和相关逻辑。DAO模式分离了业务逻辑和数据访问逻辑,使得代码更易于维护和扩展。

在 horizon 中,dao目录通常包含一些文件,如dao.go等,其中包含与数据库交互的方法和函数。这些方法和函数可以在其他代码中被引用,用于数据的读取、写入和更新等操作。

这样可以保持代码的清晰,逻辑明确。

dao 的设计如下,我们以其中的 registry 目录为例:

包含了接口作为抽象层,供外部调用:

type DAO interface {
	// Create a registry
	Create(ctx context.Context, registry *models.Registry) (uint, error)
	// UpdateByID update a registry
	UpdateByID(ctx context.Context, id uint, registry *models.Registry) error
	// DeleteByID delete a registry by id
	DeleteByID(ctx context.Context, id uint) error
	// GetByID get by id
	GetByID(ctx context.Context, id uint) (*models.Registry, error)
	// ListAll list all registries
	ListAll(ctx context.Context) ([]*models.Registry, error)
}

DAO 接口中的 CreateUpdateByIDDeleteByIDGetByID 和 ListAll 方法都是对 registry 表进行了不同的操作。Create 方法是用来创建一个新的 registry,UpdateByID 方法是用来更新 registry 表中指定 ID 的记录,DeleteByID 方法是用来删除 registry 表中指定 ID 的记录,GetByID 方法是用来获取 registry 表中指定 ID 的记录,而 ListAll 方法是用来获取 registry 表中所有记录的列表。

DAO 接口定义了对 registry 表的增删改查操作,而 dao 结构体实现了这些接口。它都使用了 GORM 库来操作数据库:

type dao struct{ db *gorm.DB }
// NewDAO returns an instance of the default DAO
func NewDAO(db *gorm.DB) DAO {
	return &dao{db: db}
}

还有一个 NewDAO 函数,返回的是一个接口,因为 DAO 将数据层面和业务层面剥开了。

需要 NewDAO 函数的原因是因为 DAO 接口和 dao 结构体是分离的。DAO 接口只定义了对 registry 表的增删改查操作,而 dao 结构体实现了这些操作。为了方便使用 DAO 接口,我们需要一个函数来创建 dao 结构体的实例,这个实例可以被用来调用 DAO 接口中定义的方法。

在 GORM 中,我们需要使用一个数据库连接来进行操作。因此,NewDAO 函数需要一个 GORM 数据库连接作为参数。这个数据库连接可以通过 GORM 库的 Open 函数来创建,然后再传递给 NewDAO 函数。这样就可以创建一个 dao 结构体的实例,用来对数据库中的 registry 表进行 CURD 操作了。

manager

我一直很好奇为什么我们有了 dao 还需要 manager , 这里使用了 DAO 设计模式和依赖注入(DI)设计模式。

package manager

import (
	"context"

	"github.com/horizoncd/horizon/pkg/registry/dao"
	"github.com/horizoncd/horizon/pkg/registry/models"
	"gorm.io/gorm"
)

type Manager interface {
	// Create a registry
	Create(ctx context.Context, registry *models.Registry) (uint, error)
	// UpdateByID update a registry
	UpdateByID(ctx context.Context, id uint, registry *models.Registry) error
	// DeleteByID delete a registry by id
	DeleteByID(ctx context.Context, id uint) error
	// GetByID get by id
	GetByID(ctx context.Context, id uint) (*models.Registry, error)
	// ListAll list all registries
	ListAll(ctx context.Context) ([]*models.Registry, error)
}

type manager struct {
	registryDAO dao.DAO
}

func New(db *gorm.DB) Manager {
	return &manager{
		registryDAO: dao.NewDAO(db),
	}
}

func (m manager) Create(ctx context.Context, registry *models.Registry) (uint, error) {
	return m.registryDAO.Create(ctx, registry)
}

func (m manager) GetByID(ctx context.Context, id uint) (*models.Registry, error) {
	return m.registryDAO.GetByID(ctx, id)
}

func (m manager) ListAll(ctx context.Context) ([]*models.Registry, error) {
	return m.registryDAO.ListAll(ctx)
}

func (m manager) UpdateByID(ctx context.Context, id uint, registry *models.Registry) error {
	return m.registryDAO.UpdateByID(ctx, id, registry)
}

func (m manager) DeleteByID(ctx context.Context, id uint) error {
	return m.registryDAO.DeleteByID(ctx, id)
}

manager 目录实现了 Manager 接口。Manager 接口是对 DAO 接口的进一步封装,提供了更高层次的抽象。它定义了对 registry 表的增删改查操作,这些操作是通过 DAO 接口来实现的。

它对 DAO 接口进行了进一步的封装,提供了更高层次的抽象。这样就可以将 DAO 接口与具体的实现分离,使代码更加清晰易懂。

当然,在这里 horizon 实现的功能就是这么一些,除此之外,它还可以对 DAO 接口中的方法进行统一处理,例如进行错误处理和日志记录。这样就可以提高代码的可维护性和可读性。

models

models 包定义了与 registry 表相关的数据结构。它包含了一个名为 Registry 的结构体,用来表示 registry 表中的一条记录。

package models

import (
	"github.com/horizoncd/horizon/pkg/server/global"
)

type Registry struct {
	global.Model

	Name   string
	Server string
	Path   string
	Token  string
	// for delete
	InsecureSkipTLSVerify bool `gorm:"column:insecure_skip_tls_verify"`
	Kind                  string
}

Registry 结构体的字段与 registry 表中的字段一一对应。它包含了 ID、Name、Server、Path、Token、InsecureSkipTLSVerify 和 Kind 七个字段。

  • ID:registry 记录的唯一标识符。
  • CreatedAt:registry 记录的创建时间。
  • UpdatedAt:registry 记录的更新时间。
  • DeletedAt:registry 记录的删除时间。
  • Name:registry 的名称。
  • Server:registry 的服务器地址。
  • Path:registry 的地址路径。
  • Token:registry 的访问令牌。
  • InsecureSkipTLSVerify:是否忽略 TLS 验证。
  • Kind:registry 的类型。

Registry 结构体还定义了 TableName 方法,用来指定该结构体对应的表名。

Registry 结构体中的这些字段都是通过 GORM 库的 tags 来映射到数据库中的表和列的。例如,ID 字段映射到了 registry 表中的 id 列,Name 字段映射到了 registry 表中的 name 列。

该 models 包的作用是定义了与 registry 表相关的数据结构,用于在 DAO 和 manager 中进行数据的传递和操作。

设计思想

和 registry 目录类似,我们使用更加强大的 user 目录分析:

❯ tree user
user
├── dao
│   └── dao.go
├── manager
│   ├── manager.go
│   └── manager_test.go
├── models
│   └── user.go
├── service
│   ├── service.go
│   └── service_test.go
└── util
    └── session.go

dao

dao 目录中包含了与用户信息相关的数据访问对象(Data Access Object)。它定义了一个 DAO 接口,用于对 user 表进行增删改查操作。dao 目录的主要作用是封装了对数据库的访问,使得业务逻辑和数据访问逻辑分离开来。

manager

manager 目录中包含了与用户信息相关的业务逻辑。它定义了一个 Manager 接口,用于对 user 表进行增删改查操作。manager 目录的主要作用是将 DAO 接口进一步封装,提供了更高层次的抽象。

models

models 目录中定义了与用户信息相关的数据结构。它包含了一个名为 User 的结构体,用来表示 user 表中的一条记录。User 结构体的字段与 user 表中的字段一一对应。它还定义了 TableName 方法,用来指定该结构体对应的表名。

service

type Service interface {
	// CheckUsersExists check users all exists, if true, return nil
	CheckUsersExists(ctx context.Context, emails []string) error
}

type service struct {
	userManager usermanager.Manager
}

service 目录中包含了与用户信息相关的服务。它定义了一个 Service 接口,用于对 user 表进行增删改查操作。service 目录的主要作用是将业务逻辑(manager) 进一步封装,提供了更高层次的抽象。

util

util 目录中包含了与用户信息相关的工具函数。它定义了一个名为 Session 的结构体,用于表示用户的会话信息。Session 结构体的字段包括了用户的 ID、登录时间和过期时间等信息。它还包含了一些方法,用于生成和验证会话信息。util 目录的主要作用是封装了与会话相关的操作。

设计模式的设计思路和方法

DAO 设计模式是一种用于将数据访问逻辑与业务逻辑分离的设计模式。它的主要思想是将数据访问逻辑封装到一个单独的类或接口中,并由一个或多个业务逻辑组件使用它。在这个案例中,dao 包定义了 DAO 接口和 dao 结构体,它们分别用于定义和实现对 registry 表的增删改查操作。Manager 接口和 manager 结构体是 DAO 接口的高层次封装,它们进一步将数据访问逻辑和业务逻辑分离开来。这样就可以使代码更加清晰易懂,并且方便维护和扩展。

依赖注入(DI)设计模式是一种用于解耦组件之间依赖关系的设计模式。它的主要思想是将一个组件所依赖的其他组件通过构造函数、方法参数或属性注入的方式来实现。在这个案例中,NewDAO 函数使用了 GORM 数据库连接作为参数,然后将这个参数传递给 dao.NewDAO 函数来创建 dao 结构体的实例。这样就可以将数据库连接与 dao 结构体解耦开来,使代码更加灵活和可扩展。

rbac 目录

作为一个访问控制颗粒度仅仅次于 ABAC 的 RBAC,我们看一下它的源码:

目录结构:

├── rbac
│   ├── auth.go
│   ├── auth_test.go
│   ├── role
│   │   ├── roles.go
│   │   └── roles_test.go
│   └── types
│       ├── eveluation_helpers.go
│       ├── types.go
│       └── types_test.go

📜 对上面的解释:

roles

type CompResult uint8

const (
	RoleEqual CompResult = iota
	RoleBigger
	RoleSmaller
	RoleCanNotCompare
)

const (
	PE         string = "pe"
	Owner      string = "owner"
	Maintainer string = "maintainer"
	Guest      string = "guest"
)

var (
	ErrorRoleNotFound   = errors.New("role not found")
	ErrorLoadCheckError = errors.New("load check error")
)

type Service interface {
	// ListRole Lists all the role
	ListRole(ctx context.Context) ([]types.Role, error)
	// GetRole get role by the role name
	GetRole(ctx context.Context, roleName string) (*types.Role, error)
	// RoleCompare compare if the role1's permission is higher than role2
	RoleCompare(ctx context.Context, role1, role2 string) (CompResult, error)
	// GetDefaultRole return the default role(no default role will return nil)
	GetDefaultRole(ctx context.Context) *types.Role
}

type roleRankMapItem struct {
	rank int
	role types.Role
}
type fileRoleService struct {
	RolePriorityRankDesc []string
	DefaultRoleName      string
	Roles                []types.Role
	DefaultRole          *types.Role
	roleRankMap          map[string]roleRankMapItem
}

roles

roles.go 文件定义了 Service 接口及其默认实现 fileRoleService,这是一个基于文件的角色服务,它从 JSON 文件中读取角色信息。

Service 接口定义了以下方法:

  • ListRole:列出所有角色。
  • GetRole:根据角色名称获取角色信息。
  • RoleCompare:比较两个角色的优先级。
  • GetDefaultRole:获取默认角色。

fileRoleService 结构体实现了 Service 接口 ~

  • RolePriorityRankDesc:表示角色优先级的字符串切片。
  • DefaultRoleName:表示默认角色的名称。
  • Roles:表示所有角色的 []Role 类型的切片。
  • DefaultRole:表示默认角色的 Role 类型。

这个结构体还包含一个私有字段 roleRankMap,用于缓存角色的优先级信息。

Roles 字段中存储了所有角色的信息,每个角色都是一个 Role 结构体,包含角色名称、描述和权限规则。其中 PolicyRules 字段是一个 []PolicyRule 类型的切片,表示该角色的所有权限规则。

GetRole 方法根据角色名称获取角色信息。如果指定名称的角色不存在,则返回错误 ErrorRoleNotFound

RoleCompare 方法比较两个角色的优先级。如果角色 role1 的权限优先级高于 role2,则返回结果 RoleBigger;如果 role1 的权限优先级低于 role2,则返回结果 RoleSmaller。如果两个角色的权限优先级相等,则返回结果 RoleEqual。如果其中一个角色不存在,则返回错误 ErrorRoleNotFound

GetDefaultRole 方法返回默认角色。如果没有默认角色,则返回 nil

auth

// Authorizer use the basic rbac rules to check if the user
// have the permissions
type Authorizer interface {
	Authorize(ctx context.Context, attributes auth.Attributes) (auth.Decision, string, error)
}

type VisitorFunc func(fmt.Stringer, *types.PolicyRule, error) bool

func NewAuthorizer(roleservice role.Service, memberservice memberservice.Service) Authorizer {
	return &authorizer{
		roleService:   roleservice,
		memberService: memberservice,
	}
}

type authorizer struct {
	roleService   role.Service
	memberService memberservice.Service
}

const (
	NotChecked        = "not checked"
	ResourceFormatErr = "format error"
	AnonymousUser     = "anonymous user"
	InternalError     = "internal error"
	MemberNotExist    = "member not exist"
	RoleNotExist      = "role not exist"
	AdminAllow        = "admin allows everything"
)

auth 包定义了用于身份验证和访问控制的代码。在这个包中,auth.go 文件定义了 Authorizer 接口及其默认实现 authorizer,用于检查用户是否具有操作资源的权限。

Authorizer 接口:

Authorizer 接口定义了一个 Authorize 方法,用于检查用户是否具有操作资源的权限。该方法接受一个 auth.Attributes 类型的参数,该参数包含用户的身份信息(如用户名、角色等)以及资源信息(如资源名称、操作方式等)。该方法返回一个 auth.Decision 类型的决策结果、一个字符串类型的消息以及一个 error 类型的错误。其中 auth.Decision 类型表示决策的结果,可以是 auth.Allowauth.Denyauth.Abstain

authorizer 结构体:

authorizer 结构体实现了 Authorizer 接口,它的字段包括 roleServicememberService。其中 roleService 是一个 role.Service 类型的接口,用于获取角色信息,memberService 是一个 memberservice.Service 类型的接口,用于获取用户信息。

authorizer 结构体还包含一些常量,如 NotCheckedResourceFormatErrAnonymousUserInternalErrorMemberNotExistRoleNotExist,分别表示未检查、资源格式错误、匿名用户、内部错误、用户不存在和角色不存在。

authorizer 结构体实现了 Authorize 方法,该方法首先检查资源格式是否正确,然后获取用户和角色信息,最后根据角色和权限规则判断用户是否具有操作资源的权限。

types 包详解

types 包定义了与角色和权限相关的数据类型。在这个包中,types.go 文件定义了 RolePolicyRule 结构体。

Role 结构体:

Role 结构体表示一个角色,包含角色的名称、描述和权限规则。定义如下:

type Role struct {
    Name        string       `yaml:"name" json:"name"`
    Desc        string       `yaml:"desc" json:"desc"`
    PolicyRules []PolicyRule `yaml:"rules" json:"rules"`
}

其中 Name 字段表示角色名称,Desc 字段表示角色描述,PolicyRules 字段是一个 []PolicyRule 类型的切片,表示该角色的所有权限规则。

PolicyRule 结构体:

PolicyRule 结构体表示一个权限规则,包含权限的访问方式、资源、操作范围等信息。定义如下:

type PolicyRule struct {
    Verbs           []string `yaml:"verbs" json:"verbs"`
    APIGroups       []string `yaml:"apiGroups" json:"apiGroups"`
    Resources       []string `yaml:"resources" json:"resources"`
    Scopes          []string `yaml:"scopes" json:"scopes"`
    NonResourceURLs []string `yaml:"nonResourceURLs" json:"nonResourceURLs"`
}

其中 Verbs 字段是一个字符串切片,表示权限的访问方式(例如 get、list、create、update 等),APIGroups 字段是一个字符串切片,表示资源所属的 API 组,Resources 字段是一个字符串切片,表示资源名称,Scopes 字段是一个字符串切片,表示操作的范围(例如 namespace 等),NonResourceURLs 字段是一个字符串切片,表示非资源 URL。

types.go 文件中还定义了一些常量:

const (
    APIGroupAll    = "*"
    ResourceAll    = "*"
    VerbAll        = "*"
    ScopeAll       = "*"
    NonResourceAll = "*"
)

其中 APIGroupAllResourceAllVerbAllScopeAllNonResourceAll 分别表示所有 API 组、所有资源、所有访问方式、所有操作范围、所有非资源 URL。

END 链接