Harbor 源碼淺析

Harbor 源碼淺析

Harbor 是一個CNCF基金會託管的開源的可信的雲原生docker registry項目,可以用於存儲、簽名、掃描鏡像內容,Harbor 通過添加一些常用的功能如安全性、身份權限管理等來擴展 docker registry 項目,此外還支持在 registry 之間複製鏡像,還提供更加高級的安全功能,如用戶管理、訪問控制和活動審計等,在新版本中還添加了Helm倉庫託管的支持。

本文所有源碼基於 Harbor release-1.7.0 版本進行分析。

Harbor最核心的功能就是給 docker registry 添加上一層權限保護的功能,要實現這個功能,就需要我們在使用 docker login、pull、push 等命令的時候進行攔截,先進行一些權限相關的校驗,再進行操作,其實這一系列的操作 docker registry v2 就已經為我們提供了支持,v2 集成了一個安全認證的功能,將安全認證暴露給外部服務,讓外部服務去實現。

docker registry v2 認證

上面我們說了 docker registry v2 將安全認證暴露給了外部服務使用,那麼是怎樣暴露的呢?我們在命令行中輸入docker login https://registry.qikqiak.com為例來為大家說明下認證流程:

  • 1. docker client 接收到用戶輸入的 docker login 命令,將命令轉化為調用 engine api 的 RegistryLogin 方法
  • 2. 在 RegistryLogin 方法中通過 http 盜用 registry 服務中的 auth 方法
  • 3. 因為我們這裡使用的是 v2 版本的服務,所以會調用 loginV2 方法,在 loginV2 方法中會進行 /v2/ 接口調用,該接口會對請求進行認證
  • 4. 此時的請求中並沒有包含 token 信息,認證會失敗,返回 401 錯誤,同時會在 header 中返回去哪裡請求認證的服務器地址
  • 5. registry client 端收到上面的返回結果後,便會去返回的認證服務器那裡進行認證請求,向認證服務器發送的請求的 header 中包含有加密的用戶名和密碼
  • 6. 認證服務器從 header 中獲取到加密的用戶名和密碼,這個時候就可以結合實際的認證系統進行認證了,比如從數據庫中查詢用戶認證信息或者對接 ldap 服務進行認證校驗
  • 7. 認證成功後,會返回一個 token 信息,client 端會拿著返回的 token 再次向 registry 服務發送請求,這次需要帶上得到的 token,請求驗證成功,返回狀態碼就是200了
  • 8. docker client 端接收到返回的200狀態碼,說明操作成功,在控制檯上打印Login Succeeded的信息

至此,整個登錄過程完成,整個過程可以用下面的流程圖來說明:

Harbor 源碼淺析

要完成上面的登錄認證過程有兩個關鍵點需要注意:怎樣讓 registry 服務知道服務認證地址?我們自己提供的認證服務生成的 token 為什麼 registry 就能夠識別?

對於第一個問題,比較好解決,registry 服務本身就提供了一個配置文件,可以在啟動 registry 服務的配置文件中指定上認證服務地址即可,其中有如下這樣的一段配置信息:

......
auth:
token:
realm: token-realm
service: token-service
issuer: registry-token-issuer
rootcertbundle: /root/certs/bundle
......

其中 realm 就可以用來指定一個認證服務的地址,下面我們可以看到 Harbor 中該配置的內容

關於 registry 的配置,可以參考官方文檔:https://docs.docker.com/registry/configuration/

第二個問題,就是 registry 怎麼能夠識別我們返回的 token 文件?如果按照 registry 的要求生成一個 token,是不是 registry 就可以識別了?所以我們需要在我們的認證服務器中按照 registry 的要求生成 token,而不是隨便亂生成。那麼要怎麼生成呢?我們可以在 docker registry 的源碼中可以看到 token 是如何定義的,文件路徑在distribution/registry/token/token.go,從源碼中我們可以看到 token 是通過JWT(JSON Web Token)來實現的,所以我們按照要求生成一個 JWT 的 token 就可以了。

Harbor 認證

上面我們已經說明了 docker registry v2 認證的整個流程,Harbor 實際上核心的功能就是提供上面的認證服務的功能。我們在 Harbor 的源碼目錄中可以查看到 registry 服務的配置文件,路徑為:make/common/templates/registry/config.yml,其中有兩個非常重要的配置信息:

......
auth:
token:
issuer: harbor-token-issuer
realm: $public_url/service/token
rootcertbundle: /etc/registry/root.crt
service: harbor-registry
......

一個就是上面我們提到的 auth.token.realm,是用來提供 registry v2 安全認證的外部服務地址,這裡默認的配置是$public_url/service/token,其中$public_url就是 Harbor 服務的主域地址,所以安全認證服務就是去請求/service/token這個地址了,由於 Harbor 是基於 beego 這個 web 框架進行開發的,所以我們只需要去查找下/service/token這個路由,就可以找到對應的請求處理方法了。可以很容易在文件src/core/router.go文件中找到改路由:

func initRouters() {
......
beego.Router("/service/token", &token.Handler{})
......
}

上面的請求處理方法在src/core/service/token.go文件中,裡面有一個Get方法就是用來處理該請求的:

func (h *Handler) Get() {
request := h.Ctx.Request
log.Debugf("URL for token request: %s", request.URL.String())
service := h.GetString("service")
tokenCreator, ok := creatorMap[service]
if !ok {
errMsg := fmt.Sprintf("Unable to handle service: %s", service)
log.Errorf(errMsg)
h.CustomAbort(http.StatusBadRequest, errMsg)
}
token, err := tokenCreator.Create(request)
if err != nil {
if _, ok := err.(*unauthorizedError); ok {
h.CustomAbort(http.StatusUnauthorized, "")
}
log.Errorf("Unexpected error when creating the token, error: %v", err)
h.CustomAbort(http.StatusInternalServerError, "")
}
h.Data["json"] = token
h.ServeJSON()
}

上面的方法通過參數 service 來獲取一個 tokenCreator,然後調用 Create 方法生成 token,方法如下:

func (g generalCreator) Create(r *http.Request) (*models.Token, error) {
var err error
scopes := parseScopes(r.URL)
log.Debugf("scopes: %v", scopes)
ctx, err := filter.GetSecurityContext(r)
if err != nil {
return nil, fmt.Errorf("failed to get security context from request")
}
pm, err := filter.GetProjectManager(r)
if err != nil {
return nil, fmt.Errorf("failed to get project manager from request")
}
// for docker login
if !ctx.IsAuthenticated() {
if len(scopes) == 0 {
return nil, &unauthorizedError{}
}
}
access := GetResourceActions(scopes)
err = filterAccess(access, ctx, pm, g.filterMap)
if err != nil {
return nil, err

}
return MakeToken(ctx.GetUsername(), g.service, access)
}

這裡就做了一系列的權限校驗,如果沒有問題就生成一個 token 對象返回,這裡生成的 Token 對象結構體如下:

type Token struct {
Token string `json:"token"`
ExpiresIn int `json:"expires_in"`
IssuedAt string `json:"issued_at"`
}

和 JWT 定義的 token 格式是保持一致的,所以 docker registry v2 能夠識別我們返回的 token 字符串。

Harbor API

上面是 Harbor 提供的最核心的認證服務功能,除此之外還有很多其他的功能,比如 Harbor 還提供了一個額外的 Dashboard 可供我們操作,還支持 Helm Chart 倉庫。同樣我們再看下之前的src/core/router.go文件:

func initRouters() {
// standalone
if !config.WithAdmiral() {
// Controller API:
beego.Router("/c/login", &controllers.CommonController{}, "post:Login")
beego.Router("/c/log_out", &controllers.CommonController{}, "get:LogOut")
beego.Router("/c/reset", &controllers.CommonController{}, "post:ResetPassword")
beego.Router("/c/userExists", &controllers.CommonController{}, "post:UserExists")
beego.Router("/c/sendEmail", &controllers.CommonController{}, "get:SendResetEmail")
// API:
beego.Router("/api/projects/:pid([0-9]+)/members/?:pmid([0-9]+)", &api.ProjectMemberAPI{})
beego.Router("/api/projects/", &api.ProjectAPI{}, "head:Head")
beego.Router("/api/projects/:id([0-9]+)", &api.ProjectAPI{})
beego.Router("/api/users/:id", &api.UserAPI{}, "get:Get;delete:Delete;put:Put")
beego.Router("/api/users", &api.UserAPI{}, "get:List;post:Post")

beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword")
beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole")
beego.Router("/api/usergroups/?:ugid([0-9]+)", &api.UserGroupAPI{})
beego.Router("/api/ldap/ping", &api.LdapAPI{}, "post:Ping")
beego.Router("/api/ldap/users/search", &api.LdapAPI{}, "get:Search")
beego.Router("/api/ldap/groups/search", &api.LdapAPI{}, "get:SearchGroup")
beego.Router("/api/ldap/users/import", &api.LdapAPI{}, "post:ImportUser")
beego.Router("/api/email/ping", &api.EmailAPI{}, "post:Ping")
}
// API
beego.Router("/api/ping", &api.SystemInfoAPI{}, "get:Ping")
beego.Router("/api/search", &api.SearchAPI{})
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post")
beego.Router("/api/projects/:id([0-9]+)/logs", &api.ProjectAPI{}, "get:Logs")
beego.Router("/api/projects/:id([0-9]+)/_deletable", &api.ProjectAPI{}, "get:Deletable")
beego.Router("/api/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get")
beego.Router("/api/projects/:id([0-9]+)/metadatas/", &api.MetadataAPI{}, "post:Post")
beego.Router("/api/projects/:id([0-9]+)/metadatas/:name", &api.MetadataAPI{}, "put:Put;delete:Delete")
beego.Router("/api/repositories", &api.RepositoryAPI{}, "get:Get")
beego.Router("/api/repositories/scanAll", &api.RepositoryAPI{}, "post:ScanAll")
beego.Router("/api/repositories/*", &api.RepositoryAPI{}, "delete:Delete;put:Put")
beego.Router("/api/repositories/*/labels", &api.RepositoryLabelAPI{}, "get:GetOfRepository;post:AddToRepository")
beego.Router("/api/repositories/*/labels/:id([0-9]+)", &api.RepositoryLabelAPI{}, "delete:RemoveFromRepository")
beego.Router("/api/repositories/*/tags/:tag", &api.RepositoryAPI{}, "delete:Delete;get:GetTag")
beego.Router("/api/repositories/*/tags/:tag/labels", &api.RepositoryLabelAPI{}, "get:GetOfImage;post:AddToImage")
beego.Router("/api/repositories/*/tags/:tag/labels/:id([0-9]+)", &api.RepositoryLabelAPI{}, "delete:RemoveFromImage")
beego.Router("/api/repositories/*/tags", &api.RepositoryAPI{}, "get:GetTags;post:Retag")
beego.Router("/api/repositories/*/tags/:tag/scan", &api.RepositoryAPI{}, "post:ScanImage")
beego.Router("/api/repositories/*/tags/:tag/vulnerability/details", &api.RepositoryAPI{}, "Get:VulnerabilityDetails")
beego.Router("/api/repositories/*/tags/:tag/manifest", &api.RepositoryAPI{}, "get:GetManifests")
beego.Router("/api/repositories/*/signatures", &api.RepositoryAPI{}, "get:GetSignatures")
beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos")
beego.Router("/api/jobs/replication/", &api.RepJobAPI{}, "get:List;put:StopJobs")
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/jobs/scan/:id([0-9]+)/log", &api.ScanJobAPI{}, "get:GetLog")
beego.Router("/api/system/gc", &api.GCAPI{}, "get:List")
beego.Router("/api/system/gc/:id", &api.GCAPI{}, "get:GetGC")
beego.Router("/api/system/gc/:id([0-9]+)/log", &api.GCAPI{}, "get:GetLog")
beego.Router("/api/system/gc/schedule", &api.GCAPI{}, "get:Get;put:Put;post:Post")
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/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/logs", &api.LogAPI{})
beego.Router("/api/configs", &api.ConfigAPI{}, "get:GetInternalConfig")

beego.Router("/api/configurations", &api.ConfigAPI{})
beego.Router("/api/configurations/reset", &api.ConfigAPI{}, "post:Reset")
beego.Router("/api/statistics", &api.StatisticAPI{})
beego.Router("/api/replications", &api.ReplicationAPI{})
beego.Router("/api/labels", &api.LabelAPI{}, "post:Post;get:List")
beego.Router("/api/labels/:id([0-9]+)", &api.LabelAPI{}, "get:Get;put:Put;delete:Delete")
beego.Router("/api/labels/:id([0-9]+)/resources", &api.LabelAPI{}, "get:ListResources")
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/internal/syncregistry", &api.InternalAPI{}, "post:SyncRegistry")
beego.Router("/api/internal/renameadmin", &api.InternalAPI{}, "post:RenameAdmin")
beego.Router("/api/internal/configurations", &api.ConfigAPI{}, "get:GetInternalConfig")
// external service that hosted on harbor process:
beego.Router("/service/notifications", &registry.NotificationHandler{})
beego.Router("/service/notifications/clair", &clair.Handler{}, "post:Handle")
beego.Router("/service/notifications/jobs/scan/:id([0-9]+)", &jobs.Handler{}, "post:HandleScan")
beego.Router("/service/notifications/jobs/replication/:id([0-9]+)", &jobs.Handler{}, "post:HandleReplication")
beego.Router("/service/notifications/jobs/adminjob/:id([0-9]+)", &admin.Handler{}, "post:HandleAdminJob")
beego.Router("/service/token", &token.Handler{})
beego.Router("/v2/*", &controllers.RegistryProxy{}, "*:Handle")
// APIs for chart repository
if config.WithChartMuseum() {
// Charts are controlled under projects
chartRepositoryAPIType := &api.ChartRepositoryAPI{}
beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus")
beego.Router("/api/chartrepo/:repo/charts", chartRepositoryAPIType, "get:ListCharts")
beego.Router("/api/chartrepo/:repo/charts/:name", chartRepositoryAPIType, "get:ListChartVersions")
beego.Router("/api/chartrepo/:repo/charts/:name", chartRepositoryAPIType, "delete:DeleteChart")
beego.Router("/api/chartrepo/:repo/charts/:name/:version", chartRepositoryAPIType, "get:GetChartVersion")
beego.Router("/api/chartrepo/:repo/charts/:name/:version", chartRepositoryAPIType, "delete:DeleteChartVersion")
beego.Router("/api/chartrepo/:repo/charts", chartRepositoryAPIType, "post:UploadChartVersion")
beego.Router("/api/chartrepo/:repo/prov", chartRepositoryAPIType, "post:UploadChartProvFile")
beego.Router("/api/chartrepo/charts", chartRepositoryAPIType, "post:UploadChartVersion")
// Repository services
beego.Router("/chartrepo/:repo/index.yaml", chartRepositoryAPIType, "get:GetIndexByRepo")
beego.Router("/chartrepo/index.yaml", chartRepositoryAPIType, "get:GetIndex")
beego.Router("/chartrepo/:repo/charts/:filename", chartRepositoryAPIType, "get:DownloadChart")
// Labels for chart
chartLabelAPIType := &api.ChartLabelAPI{}
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels", chartLabelAPIType, "get:GetLabels;post:MarkLabel")
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel")
}
// Error pages
beego.ErrorController(&controllers.ErrorController{})
}

上面這個文件裡面就定義了 Harbor 核心的一些 API,其中如果!config.WithAdmiral()為真,則定義的一些用登錄相關接口就會生效,Admiral 是 Vmware 的一個容器管理平臺,如果我們在配置文件中定義了參數admiral_url,那麼 Harbor 就會和 Admiral 進行交互,如果沒有配置這個參數,那麼就會去和我們定義的 login 相關接口進行交互了。我們這裡簡單介紹一下主要的接口,第一個登錄接口post:Login:

// Login handles login request from UI.
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("user", *user)
}

根據請求獲取用戶名和密碼,然後調用auth.Login方法進行登錄校驗,方法位於文件src/core/auth/authenticator.go下:

// Login authenticates user credentials based on setting.
func Login(m models.AuthModel) (*models.User, error) {
authMode, err := config.AuthMode()
if err != nil {
return nil, err
}
if authMode == "" || dao.IsSuperUser(m.Principal) {
authMode = common.DBAuth
}
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 err != nil {
if _, ok = err.(ErrAuth); ok {
log.Debugf("Login failed, locking %s, and sleep for %v", m.Principal, frozenTime)
lock.Lock(m.Principal)
time.Sleep(frozenTime)
}

return nil, err
}
err = authenticator.PostAuthenticate(user)
return user, err
}

通過authenticator.Authenticate方法進行驗證,這裡就需要通過 authMode 來進行判斷應該調用哪個認證方法 驗證,該參數就是 Harbor 全局配置文件中的auth_mode參數,默認情況下auth_mode=db_auth,除此之外還可以設置成ldap_auth來通過提供一個LDAP Server進行用戶認證,也可以設置成uaa_auth來通過 cloud foundry 的 id manager 來進行用戶認證。比如如果使用數據庫驗證的話,那麼校驗方法就在文件src/core/auth/db/db.go中,方法如下:

// Authenticate calls dao to authenticate user.
func (d *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
u, err := dao.LoginByDb(m)
if err != nil {
return nil, err
}
if u == nil {
return nil, auth.NewErrAuth("Invalid credentials")
}
return u, nil
}

進入LoginByDb方法,位於src/common/dao/user.go文件:

// LoginByDb is used for user to login with database auth mode.
func LoginByDb(auth models.AuthModel) (*models.User, error) {
o := GetOrmer()
var users []models.User
n, err := o.Raw(`select * from harbor_user where (username = ? or email = ?) and deleted = false`,
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
}

上面這段代碼邏輯就很簡單了,首先根據用戶名獲取用戶,然後將密碼加密進行比較,驗證通過就將 user 對象返回,並保存到 session 裡面,這個後面會用到。

其它的 API 操作類似,在此不再一一講述,另外再和大家介紹一下鏡像倉庫的相關 API 操作,鏡像操作相關 API 主要位於/api/repositories下面,請求處理方法主要位於文件src/ui/api/repository.go中,比如獲取鏡像分頁列表數據:

func (ra *RepositoryAPI) Get() {
projectID, err := ra.GetInt64("project_id")
if err != nil || projectID <= 0 {
ra.HandleBadRequest(fmt.Sprintf("invalid project_id %s", ra.GetString("project_id")))
return
}
labelID, err := ra.GetInt64("label_id", 0)
if err != nil {
ra.HandleBadRequest(fmt.Sprintf("invalid label_id: %s", ra.GetString("label_id")))
return
}
exist, err := ra.ProjectMgr.Exists(projectID)
if err != nil {
ra.ParseAndHandleError(fmt.Sprintf("failed to check the existence of project %d",
projectID), err)
return
}
if !exist {
ra.HandleNotFound(fmt.Sprintf("project %d not found", projectID))
return
}
if !ra.SecurityCtx.HasReadPerm(projectID) {
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()

return
}
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
return
}
query := &models.RepositoryQuery{
ProjectIDs: []int64{projectID},
Name: ra.GetString("q"),
LabelID: labelID,
}
query.Page, query.Size = ra.GetPaginationParams()
query.Sort = ra.GetString("sort")
total, err := dao.GetTotalOfRepositories(query)
if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get total of repositories of project %d: %v",
projectID, err))
return
}
repositories, err := getRepositories(query)
if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get repository: %v", err))
return
}
ra.SetPaginationHeader(total, query.Page, query.Size)
ra.Data["json"] = repositories
ra.ServeJSON()
}

上面的邏輯也相對比較簡單,獲取請求的參數,拼湊成一個 RepositoryQuery 對象,然後根據該對象去查詢倉庫列表數據,並支持分頁返回,查詢只是就是簡單的操作數據庫而已,其他操作也類似。不過我們仔細查看改文件中,並沒有提供創建倉庫的接口,這是因為創建 Repository 是在上傳鏡像的時候創建的,這裡又回到 registry 的配置文件,裡面有一段如下的配置:

......
notifications:
endpoints:
- name: harbor
disabled: false

url: $core_url/service/notifications
......

其中配置的 url 就是倉庫的一個回調 web hook 地址,在pull或者push鏡像後就會觸發該 hook 請求,比如 Harbor 這裡就會去請求/service/notifications這個 url:

// Post handles POST request, and records audit log or refreshes cache based on event.
func (n *NotificationHandler) Post() {
var notification models.Notification
err := json.Unmarshal(n.Ctx.Input.CopyBody(1<<32), &notification)
if err != nil {
log.Errorf("failed to decode notification: %v", err)
return
}
events, err := filterEvents(&notification)
if err != nil {
log.Errorf("failed to filter events: %v", err)
return
}
for _, event := range events {
repository := event.Target.Repository
project, _ := utils.ParseRepository(repository)
tag := event.Target.Tag
action := event.Action
user := event.Actor.Name
if len(user) == 0 {
user = "anonymous"
}
pro, err := config.GlobalProjectMgr.Get(project)
if err != nil {
log.Errorf("failed to get project by name %s: %v", project, err)
return
}
if pro == nil {
log.Warningf("project %s not found", project)
continue
}
go func() {
if err := dao.AddAccessLog(models.AccessLog{
Username: user,
ProjectID: pro.ProjectID,
RepoName: repository,
RepoTag: tag,
Operation: action,
OpTime: time.Now(),
}); err != nil {

log.Errorf("failed to add access log: %v", err)
}
}()
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,
ProjectID: pro.ProjectID,
}
if err := dao.AddRepository(repoRecord); err != nil {
log.Errorf("Error happens when adding repository: %v", err)
}
}()
if !coreutils.WaitForManifestReady(repository, tag, 5) {
log.Errorf("Manifest for image %s:%s is not ready, skip the follow up actions.", repository, tag)
return
}
go func() {
image := repository + ":" + tag
err := notifier.Publish(topic.ReplicationEventTopicOnPush, rep_notification.OnPushNotification{
Image: image,
})
if err != nil {
log.Errorf("failed to publish on push topic for resource %s: %v", image, err)
return
}
log.Debugf("the on push topic for resource %s published", image)
}()
if autoScanEnabled(pro) {
last, err := clairdao.GetLastUpdate()
if err != nil {
log.Errorf("Failed to get last update from Clair DB, error: %v, the auto scan will be skipped.", err)
} else if last == 0 {
log.Infof("The Vulnerability data is not ready in Clair DB, the auto scan will be skipped.", err)
} else if err := coreutils.TriggerImageScan(repository, tag); err != nil {
log.Warningf("Failed to scan image, repository: %s, tag: %s, error: %v", repository, tag, err)
}
}
}
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)
}

}()
}
}
}

從上面代碼中可以看到首先在 hook 中我們可以獲取到當前操作的動作,如果是 push 操作,首先判斷 repository 是否存在,如果不存在則創建,對於 pull 鏡像操作通過 IncreasePullCount 更新數據庫 pull 鏡像次數:

// IncreasePullCount ...
func IncreasePullCount(name string) (err error) {
o := GetOrmer()
num, err := o.QueryTable("repository").Filter("name", name).Update(
orm.Params{
"pull_count": orm.ColValue(orm.ColAdd, 1),
"update_time": time.Now(),
})
if err != nil {
return err
}
if num == 0 {
return fmt.Errorf("Failed to increase repository pull count with name: %s", name)
}
return nil
}

除此之外,在路由文件中還可以看到config.WithChartMuseum()配置,如果在全局配置中配置了with_chartmuseum=true,則就會開啟 Helm Chart 倉庫所需要的 API,相關的請求處理方法位於文件src/core/api/chart_repository.go文件中。

除了上面的一些主要功能之外,Harbor 還有很多高級可能,感興趣的同學可以下載 Harbor 的源碼自行研究,當我們對源碼比較熟悉之後,對於我們搭建 Harbor 顯然是非常有幫助的,下節課給大家介紹怎樣在 Kubernetes 集群中來搭建 Harbor。


分享到:


相關文章: