首先看一下harbor的API,harbor代码结构还是比较简单,如果熟悉springmvc的人对此可能更加容易理解。它整个web服务是基于beego的,首先看路由src/ui/router.go。
beego.SetStaticPath("/static", "./static")
beego.SetStaticPath("/i18n", "./static/i18n")
//Page Controllers:
beego.Router("/", &controllers.IndexController{})
beego.Router("/sign-in", &controllers.IndexController{})
beego.Router("/sign-up", &controllers.IndexController{})
beego.Router("/reset_password", &controllers.IndexController{})
beego.Router("/harbor", &controllers.IndexController{})
beego.Router("/harbor/sign-in", &controllers.IndexController{})
beego.Router("/harbor/sign-up", &controllers.IndexController{})
beego.Router("/harbor/dashboard", &controllers.IndexController{})
beego.Router("/harbor/projects", &controllers.IndexController{})
beego.Router("/harbor/projects/:id/repository", &controllers.IndexController{})
beego.Router("/harbor/projects/:id/replication", &controllers.IndexController{})
beego.Router("/harbor/projects/:id/member", &controllers.IndexController{})
beego.Router("/harbor/projects/:id/log", &controllers.IndexController{})
beego.Router("/harbor/tags/:id/*", &controllers.IndexController{})
beego.Router("/harbor/users", &controllers.IndexController{})
beego.Router("/harbor/logs", &controllers.IndexController{})
beego.Router("/harbor/replications", &controllers.IndexController{})
beego.Router("/harbor/replications/endpoints", &controllers.IndexController{})
beego.Router("/harbor/replications/rules", &controllers.IndexController{})
beego.Router("/harbor/tags", &controllers.IndexController{})
beego.Router("/harbor/configs", &controllers.IndexController{})
beego.Router("/login", &controllers.CommonController{}, "post:Login")
beego.Router("/log_out", &controllers.CommonController{}, "get:LogOut")
beego.Router("/reset", &controllers.CommonController{}, "post:ResetPassword")
beego.Router("/userExists", &controllers.CommonController{}, "post:UserExists")
beego.Router("/sendEmail", &controllers.CommonController{}, "get:SendEmail")
//API:
beego.Router("/api/search", &api.SearchAPI{})
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{})
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post;head:Head")
beego.Router("/api/projects/:id([0-9]+)", &api.ProjectAPI{})
beego.Router("/api/projects/:id([0-9]+)/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic")
beego.Router("/api/projects/:id([0-9]+)/logs/filter", &api.ProjectAPI{}, "post:FilterAccessLog")
beego.Router("/api/statistics", &api.StatisticAPI{})
beego.Router("/api/users/?:id", &api.UserAPI{})
beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword")
beego.Router("/api/internal/syncregistry", &api.InternalAPI{}, "post:SyncRegistry")
beego.Router("/api/repositories", &api.RepositoryAPI{})
beego.Router("/api/repositories/*/tags/?:tag", &api.RepositoryAPI{}, "delete:Delete")
beego.Router("/api/repositories/*/tags", &api.RepositoryAPI{}, "get:GetTags")
beego.Router("/api/repositories/*/tags/:tag/manifest", &api.RepositoryAPI{}, "get:GetManifests")
beego.Router("/api/repositories/*/signatures", &api.RepositoryAPI{}, "get:GetSignatures")
beego.Router("/api/jobs/replication/", &api.RepJobAPI{}, "get:List")
beego.Router("/api/jobs/replication/:id([0-9]+)", &api.RepJobAPI{})
beego.Router("/api/jobs/replication/:id([0-9]+)/log", &api.RepJobAPI{}, "get:GetLog")
beego.Router("/api/policies/replication/:id([0-9]+)", &api.RepPolicyAPI{})
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "get:List")
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "post:Post")
beego.Router("/api/policies/replication/:id([0-9]+)/enablement", &api.RepPolicyAPI{}, "put:UpdateEnablement")
beego.Router("/api/targets/", &api.TargetAPI{}, "get:List")
beego.Router("/api/targets/", &api.TargetAPI{}, "post:Post")
beego.Router("/api/targets/:id([0-9]+)", &api.TargetAPI{})
beego.Router("/api/targets/:id([0-9]+)/policies/", &api.TargetAPI{}, "get:ListPolicies")
beego.Router("/api/targets/ping", &api.TargetAPI{}, "post:Ping")
beego.Router("/api/targets/:id([0-9]+)/ping", &api.TargetAPI{}, "post:PingByID")
beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole")
beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos")
beego.Router("/api/logs", &api.LogAPI{})
beego.Router("/api/configurations", &api.ConfigAPI{})
beego.Router("/api/configurations/reset", &api.ConfigAPI{}, "post:Reset")
beego.Router("/api/systeminfo", &api.SystemInfoAPI{}, "get:GetGeneralInfo")
beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo")
beego.Router("/api/systeminfo/getcert", &api.SystemInfoAPI{}, "get:GetCert")
beego.Router("/api/ldap/ping", &api.LdapAPI{}, "post:Ping")
beego.Router("/api/ldap/users/search", &api.LdapAPI{}, "post:Search")
beego.Router("/api/ldap/users/import", &api.LdapAPI{}, "post:ImportUser")
beego.Router("/api/email/ping", &api.EmailAPI{}, "post:Ping")
//external service that hosted on harbor process:
beego.Router("/service/notifications", &service.NotificationHandler{})
beego.Router("/service/token", &token.Handler{})
//Error pages
beego.ErrorController(&controllers.ErrorController{})
简单介绍一下主要的接口,第一个介绍登录接口,post:Login。
func (cc *CommonController) Login() {
principal := cc.GetString("principal")
password := cc.GetString("password")
user, err := auth.Login(models.AuthModel{
Principal: principal,
Password: password,
})
if err != nil {
log.Errorf("Error occurred in UserLogin: %v", err)
cc.CustomAbort(http.StatusUnauthorized, "")
}
if user == nil {
cc.CustomAbort(http.StatusUnauthorized, "")
}
cc.SetSession("userId", user.UserID)
cc.SetSession("username", user.Username)
}
登录的接口,获取用户名和密码,验证src/ui/auth/authenticator.go,
func Login(m models.AuthModel) (*models.User, error) {
authMode, err := config.AuthMode()
if err != nil {
return nil, err
}
if authMode == "" || m.Principal == "admin" {
authMode = "db_auth"
}
log.Debug("Current AUTH_MODE is ", authMode)
authenticator, ok := registry[authMode]
if !ok {
return nil, fmt.Errorf("Unrecognized auth_mode: %s", authMode)
}
if lock.IsLocked(m.Principal) {
log.Debugf("%s is locked due to login failure, login failed", m.Principal)
return nil, nil
}
user, err := authenticator.Authenticate(m)
if user == nil && err == nil {
log.Debugf("Login failed, locking %s, and sleep for %v", m.Principal, frozenTime)
lock.Lock(m.Principal)
time.Sleep(frozenTime)
}
return user, err
}
authenticator.Authenticate验证,如果这里是db数据库验证的话,就在src/ui/auth/db/db.go
func (d *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
u, err := dao.LoginByDb(m)
if err != nil {
return nil, err
}
return u, nil
}
进入LoginByDb
func LoginByDb(auth models.AuthModel) (*models.User, error) {
o := GetOrmer()
var users []models.User
n, err := o.Raw(`select * from user where (username = ? or email = ?) and deleted = 0`,
auth.Principal, auth.Principal).QueryRows(&users)
if err != nil {
return nil, err
}
if n == 0 {
return nil, nil
}
user := users[0]
if user.Password != utils.Encrypt(auth.Password, user.Salt) {
return nil, nil
}
user.Password = "" //do not return the password
return &user, nil
}
很简单就是获取用户,验证密码,由于密码是加密存储的,所以是比较加密后的,auth.Password是明文密码,user.Password是加密过后的密码。当验证完用户的后吧用户的id和name报错到session里面,这个在后面会用到。还举个例子就是用户操作的API,包括用的curd等操作,
获取用户
func (ua *UserAPI) Get() {
. . .
userList, err := dao.ListUsers(userQuery)
. . .
}
还有修改用户
func (ua *UserAPI) Put() {
. . .
user := models.User{UserID: ua.userID}
userQuery := models.User{UserID: ua.userID}
u, err := dao.GetUser(userQuery)
if err := dao.ChangeUserProfile(user); err != nil {
. . .
}
创建用户
func (ua *UserAPI) Post() {
. . .
userExist, err := dao.UserExists(user, "username")
emailExist, err := dao.UserExists(user, "email")
userID, err := dao.Register(user)
. . .
}
其它的API操作类似,在此不再一一讲述。
harbor是镜像仓库,所以还有关于镜像的接口还是要说的,主要分为两个部分,一个是镜像仓库。一个是job。
镜像仓库主要也是一些CURD。先看src/ui/api/repository.go这个里面有查询和删除的方法,
func (ra *RepositoryAPI) Get() {
projectID, err := ra.GetInt64("project_id")
if err != nil || projectID <= 0 {
ra.CustomAbort(http.StatusBadRequest, "invalid project_id")
}
project, err := dao.GetProjectByID(projectID)
if err != nil {
log.Errorf("failed to get project %d: %v", projectID, err)
ra.CustomAbort(http.StatusInternalServerError, "")
}
if project == nil {
ra.CustomAbort(http.StatusNotFound, fmt.Sprintf("project %d not found", projectID))
}
if project.Public == 0 {
var userID int
if svc_utils.VerifySecret(ra.Ctx.Request, config.JobserviceSecret()) {
userID = 1
} else {
userID = ra.ValidateUser()
}
if !checkProjectPermission(userID, projectID) {
ra.CustomAbort(http.StatusForbidden, "")
}
}
keyword := ra.GetString("q")
total, err := dao.GetTotalOfRepositoriesByProject(projectID, keyword)
if err != nil {
log.Errorf("failed to get total of repositories of project %d: %v", projectID, err)
ra.CustomAbort(http.StatusInternalServerError, "")
}
page, pageSize := ra.GetPaginationParams()
detail := ra.GetString("detail") == "1" || ra.GetString("detail") == "true"
repositories, err := getRepositories(projectID,
keyword, pageSize, pageSize*(page-1), detail)
if err != nil {
log.Errorf("failed to get repository: %v", err)
ra.CustomAbort(http.StatusInternalServerError, "")
}
ra.SetPaginationHeader(total, page, pageSize)
ra.Data["json"] = repositories
ra.ServeJSON()
}
查询并支持分页返回,查询只是操作数据库,但删除要麻烦一些,应为删除需要调用后端docker registry接口删除,代码如下
func (r *Repository) DeleteTag(tag string) error {
digest, exist, err := r.ManifestExist(tag)
if err != nil {
return err
}
if !exist {
return ®istry_error.Error{
StatusCode: http.StatusNotFound,
}
}
return r.DeleteManifest(digest)
}
深入看一下DeleteManifest
func (r *Repository) DeleteManifest(digest string) error {
req, err := http.NewRequest("DELETE", buildManifestURL(r.Endpoint.String(), r.Name, digest), nil)
if err != nil {
return err
}
resp, err := r.client.Do(req)
if err != nil {
return parseError(err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusAccepted {
return nil
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return ®istry_error.Error{
StatusCode: resp.StatusCode,
Detail: string(b),
}
}
就是调用registry去删除,如果大家对registry的api感兴趣可以参考Docker Registry HTTP API V2这个文档。
但是大家细心就会发现没有创建的接口,因为这个创建Repository是在上传镜像的时候创建的。这里又回到registry的配置文件了,
notifications:
endpoints:
- name: harbor
disabled: false
url: http://ui/service/notifications
timeout: 3000ms
threshold: 5
backoff: 1s
这里仓库回调web hook创建Repository。
func (n *NotificationHandler) Post() {
for _, event := range events {
. . .
if action == "push" {
go func() {
exist := dao.RepositoryExists(repository)
if exist {
return
}
log.Debugf("Add repository %s into DB.", repository)
repoRecord := models.RepoRecord{Name: repository, OwnerName: user, ProjectName: project}
if err := dao.AddRepository(repoRecord); err != nil {
log.Errorf("Error happens when adding repository: %v", err)
}
}()
go api.TriggerReplicationByRepository(repository, []string{tag}, models.RepOpTransfer)
}
if action == "pull" {
go func() {
log.Debugf("Increase the repository %s pull count.", repository)
if err := dao.IncreasePullCount(repository); err != nil {
log.Errorf("Error happens when increasing pull count: %v", repository)
}
}()
}
}
}
这样push镜像到仓库的时候就可以收到回调了,首先判断repository是否存在如果不存在则创建,对于pull镜像操作通过IncreasePullCount更新数据库pull镜像次数。