内容简介:本文所有源码基于 Harbor release-1.7.0 版本进行分析。
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
为例来为大家说明下认证流程:
Login Succeeded
至此,整个登录过程完成,整个过程可以用下面的流程图来说明:
要完成上面的登录认证过程有两个关键点需要注意:怎样让 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", ®istry.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 := ⊧.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), ¬ification) if err != nil { log.Errorf("failed to decode notification: %v", err) return } events, err := filterEvents(¬ification) 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。
推荐
给大家推荐一个本人精心打造的一个精品课程,现在限时优惠中: 从 Docker 到 Kubernetes 进阶
扫描下面的二维码(或微信搜索 k8s技术圈
)关注我们的微信公众帐号,在微信公众帐号中回复 加群 即可加入到我们的 kubernetes 讨论群里面共同学习。
「真诚赞赏,手留余香」
以上所述就是小编给大家介绍的《Harbor 源码浅析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web前端开发最佳实践
党建 / 机械工业出版社 / 2015-1 / 59.00元
本书贴近Web前端标准来介绍前端开发相关最佳实践,目的在于让前端开发工程师提高编写代码的质量,重视代码的可维护性和执行性能,让初级工程师从入门开始就养成一个良好的编码习惯。本书总共分五个部分13章,第一部分包括第1章和第2章,介绍前端开发的基本范畴和现状,并综合介绍前端开发的一些最佳实践;第二部分为第3-5章,讲解HTML相关的最佳实践,并简单介绍HTML5中新标签的使用;第三部分为第6-8章,介......一起来看看 《Web前端开发最佳实践》 这本书的介绍吧!