Unverified Commit 4ca482d6 authored by Wenkai Yin's avatar Wenkai Yin Committed by GitHub
Browse files

Merge pull request #7419 from wy65701436/replication_ng

Replication ng
parents fac0c2cc ddec7bd6
......@@ -98,7 +98,7 @@ VERSIONFILENAME=UIVERSION
PREPARE_VERSION_NAME=versions
#versions
REGISTRYVERSION=v2.7.1
REGISTRYVERSION=v2.7.1-patch-2819
NGINXVERSION=$(VERSIONTAG)
NOTARYVERSION=v0.6.1
CLAIRVERSION=v2.0.7
......
......@@ -4899,7 +4899,7 @@ definitions:
RobotAccountUpdate:
type: object
properties:
disable:
disabled:
type: boolean
description: The robot account is disable or enable
Permission:
......
......@@ -29,6 +29,7 @@ This guide walks you through the fundamentals of using Harbor. You'll learn how
* [Working with Helm CLI](#working-with-helm-cli)
* [Online Garbage Collection.](#online-garbage-collection)
* [View build history.](#build-history)
* [Manage robot account of a project.](#robot-account)
## Role Based Access Control(RBAC)
......@@ -597,3 +598,42 @@ Build history make it easy to see the contents of a container image, find the co
In Harbor portal, enter your project, select the repository, click on the link of tag name you'd like to see its build history, the detail page will be opened. Then switch to `Build History` tab, you can see the build history information.
![build_ history](img/build_history.png)
## Robot Account
Robot Accounts are accounts created by project admins that are intended for automated operations. They have the following limitations:
1, Robot Accounts cannot login Harbor portal
2, Robot Accounts can only perform `docker push`/`docker pull` operations with a token.
### Add a Robot Account
If you are a project admin, you can create a Robot Account by clicking "New Robot Account" in the `Robot Accounts` tab of a project, and enter a name, a description and permission.
![add_robot_account](img/robotaccount/add_robot_account.png)
![add_robot_account](img/robotaccount/add_robot_account_2.png)
> **NOTE:** The name will become `robot$<accountname>` and will be used to distinguish a robot account from a normal harbor user.
![copy_robot_account_token](img/robotaccount/copy_robot_account_token.png)
As Harbor doesn't store your account token, please make sure to copy it in the pop up dialog after creating, otherwise, there is no way to get it from Harbor.
### Configure duration of robot account
If you are a system admin, you can configure the robot account token duration in days.
![set_robot_account_token_duration](img/robotaccount/set_robot_account_token_duration.png)
### Authenticate with a robot account
To authenticate with a Robot Account, use `docker login` as below,
```
docker login harbor.io
Username: robot$accountname
Password: Thepasswordgeneratedbyprojectadmin
```
### Disable a robot account
If you are a project admin, you can disable a Robot Account by clicking "Disable Account" in the `Robot Accounts` tab of a project.
![disable_robot_account](img/robotaccount/disable_delete_robot_account.png)
### Delete a robot account
If you are a project admin, you can delete a Robot Account by clicking "Delete" in the `Robot Accounts` tab of a project.
![delete_robot_account](img/robotaccount/disable_delete_robot_account.png)
......@@ -192,7 +192,7 @@ docker-compose up -d
protocol=http
hostname=reg.mydomain.com
if [ -n "$(grep '^[^#]*https:' ./harbor.yml)"]
if [ -n "$(grep '^[^#]*https:' ./harbor.yml)" ]
then
protocol=https
fi
......
......@@ -16,6 +16,9 @@ CREATE TRIGGER robot_update_time_at_modtime BEFORE UPDATE ON robot FOR EACH ROW
CREATE TABLE oidc_user (
id SERIAL NOT NULL,
user_id int NOT NULL,
/*
Encoded secret
*/
secret varchar(255) NOT NULL,
/*
Subject and Issuer
......@@ -24,9 +27,14 @@ CREATE TABLE oidc_user (
The sub (subject) and iss (issuer) Claims, used together, are the only Claims that an RP can rely upon as a stable identifier for the End-User
*/
subiss varchar(255) NOT NULL,
/*
Encoded token
*/
token text,
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP,
PRIMARY KEY (id),
FOREIGN KEY (user_id) REFERENCES harbor_user(user_id),
UNIQUE (subiss)
);
......
......@@ -261,8 +261,12 @@ services:
dns_search: .
ports:
- {{http_port}}:80
{% if protocol == 'https' %}
- {{https_port}}:443
{% endif %}
{% if with_notary %}
- 4443:4443
{% endif %}
depends_on:
- postgresql
- registry
......
......@@ -37,3 +37,6 @@ notifications:
timeout: 3000ms
threshold: 5
backoff: 1s
compatibility:
schema1:
enabled: true
\ No newline at end of file
FROM golang:1.11
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
ENV DOCKER_BUILDTAGS include_oss include_gcs
ENV BUILDTAGS include_oss include_gcs
WORKDIR $DISTRIBUTION_DIR
COPY . $DISTRIBUTION_DIR
......
......@@ -24,6 +24,7 @@ import (
commonhttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/utils/log"
"errors"
"github.com/astaxie/beego"
)
......@@ -48,68 +49,6 @@ func (b *BaseAPI) GetInt64FromPath(key string) (int64, error) {
return strconv.ParseInt(value, 10, 64)
}
// HandleNotFound ...
func (b *BaseAPI) HandleNotFound(text string) {
log.Info(text)
b.RenderError(http.StatusNotFound, text)
}
// HandleUnauthorized ...
func (b *BaseAPI) HandleUnauthorized() {
log.Info("unauthorized")
b.RenderError(http.StatusUnauthorized, "")
}
// HandleForbidden ...
func (b *BaseAPI) HandleForbidden(text string) {
log.Infof("forbidden: %s", text)
b.RenderError(http.StatusForbidden, text)
}
// HandleBadRequest ...
func (b *BaseAPI) HandleBadRequest(text string) {
log.Info(text)
b.RenderError(http.StatusBadRequest, text)
}
// HandleStatusPreconditionFailed ...
func (b *BaseAPI) HandleStatusPreconditionFailed(text string) {
log.Info(text)
b.RenderError(http.StatusPreconditionFailed, text)
}
// HandleConflict ...
func (b *BaseAPI) HandleConflict(text ...string) {
msg := ""
if len(text) > 0 {
msg = text[0]
}
log.Infof("conflict: %s", msg)
b.RenderError(http.StatusConflict, msg)
}
// HandleInternalServerError ...
func (b *BaseAPI) HandleInternalServerError(text string) {
log.Error(text)
b.RenderError(http.StatusInternalServerError, "")
}
// ParseAndHandleError : if the err is an instance of utils/error.Error,
// return the status code and the detail message contained in err, otherwise
// return 500
func (b *BaseAPI) ParseAndHandleError(text string, err error) {
if err == nil {
return
}
log.Errorf("%s: %v", text, err)
if e, ok := err.(*commonhttp.Error); ok {
b.RenderError(e.Code, e.Message)
return
}
b.RenderError(http.StatusInternalServerError, "")
}
// Render returns nil as it won't render template
func (b *BaseAPI) Render() error {
return nil
......@@ -120,23 +59,35 @@ func (b *BaseAPI) RenderError(code int, text string) {
http.Error(b.Ctx.ResponseWriter, text, code)
}
// RenderFormattedError renders errors with well formatted style
func (b *BaseAPI) RenderFormattedError(errorCode int, errorMsg string) {
error := commonhttp.Error{
Code: errorCode,
Message: errorMsg,
}
formattedErrMsg := error.String()
log.Errorf("%s %s failed with error: %s", b.Ctx.Request.Method, b.Ctx.Request.URL.String(), formattedErrMsg)
b.RenderError(error.Code, formattedErrMsg)
}
// DecodeJSONReq decodes a json request
func (b *BaseAPI) DecodeJSONReq(v interface{}) {
func (b *BaseAPI) DecodeJSONReq(v interface{}) error {
err := json.Unmarshal(b.Ctx.Input.CopyBody(1<<32), v)
if err != nil {
log.Errorf("Error while decoding the json request, error: %v, %v",
err, string(b.Ctx.Input.CopyBody(1 << 32)[:]))
b.CustomAbort(http.StatusBadRequest, "Invalid json request")
return errors.New("Invalid json request")
}
return nil
}
// Validate validates v if it implements interface validation.ValidFormer
func (b *BaseAPI) Validate(v interface{}) {
func (b *BaseAPI) Validate(v interface{}) (bool, error) {
validator := validation.Validation{}
isValid, err := validator.Valid(v)
if err != nil {
log.Errorf("failed to validate: %v", err)
b.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
return false, err
}
if !isValid {
......@@ -144,14 +95,17 @@ func (b *BaseAPI) Validate(v interface{}) {
for _, e := range validator.Errors {
message += fmt.Sprintf("%s %s \n", e.Field, e.Message)
}
b.CustomAbort(http.StatusBadRequest, message)
return false, errors.New(message)
}
return true, nil
}
// DecodeJSONReqAndValidate does both decoding and validation
func (b *BaseAPI) DecodeJSONReqAndValidate(v interface{}) {
b.DecodeJSONReq(v)
b.Validate(v)
func (b *BaseAPI) DecodeJSONReqAndValidate(v interface{}) (bool, error) {
if err := b.DecodeJSONReq(v); err != nil {
return false, err
}
return b.Validate(v)
}
// Redirect does redirection to resource URI with http header status code.
......@@ -163,18 +117,18 @@ func (b *BaseAPI) Redirect(statusCode int, resouceID string) {
}
// GetIDFromURL checks the ID in request URL
func (b *BaseAPI) GetIDFromURL() int64 {
func (b *BaseAPI) GetIDFromURL() (int64, error) {
idStr := b.Ctx.Input.Param(":id")
if len(idStr) == 0 {
b.CustomAbort(http.StatusBadRequest, "invalid ID in URL")
return 0, errors.New("invalid ID in URL")
}
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
b.CustomAbort(http.StatusBadRequest, "invalid ID in URL")
return 0, errors.New("invalid ID in URL")
}
return id
return id, nil
}
// SetPaginationHeader set"Link" and "X-Total-Count" header for pagination request
......@@ -213,15 +167,15 @@ func (b *BaseAPI) SetPaginationHeader(total, page, pageSize int64) {
}
// GetPaginationParams ...
func (b *BaseAPI) GetPaginationParams() (page, pageSize int64) {
page, err := b.GetInt64("page", 1)
func (b *BaseAPI) GetPaginationParams() (page, pageSize int64, err error) {
page, err = b.GetInt64("page", 1)
if err != nil || page <= 0 {
b.CustomAbort(http.StatusBadRequest, "invalid page")
return 0, 0, errors.New("invalid page")
}
pageSize, err = b.GetInt64("page_size", defaultPageSize)
if err != nil || pageSize <= 0 {
b.CustomAbort(http.StatusBadRequest, "invalid page_size")
return 0, 0, errors.New("invalid page_size")
}
if pageSize > maxPageSize {
......@@ -229,5 +183,60 @@ func (b *BaseAPI) GetPaginationParams() (page, pageSize int64) {
log.Debugf("the parameter page_size %d exceeds the max %d, set it to max", pageSize, maxPageSize)
}
return page, pageSize
return page, pageSize, nil
}
// ParseAndHandleError : if the err is an instance of utils/error.Error,
// return the status code and the detail message contained in err, otherwise
// return 500
func (b *BaseAPI) ParseAndHandleError(text string, err error) {
if err == nil {
return
}
log.Errorf("%s: %v", text, err)
if e, ok := err.(*commonhttp.Error); ok {
b.RenderFormattedError(e.Code, e.Message)
return
}
b.SendInternalServerError(errors.New(""))
}
// SendUnAuthorizedError sends unauthorized error to the client.
func (b *BaseAPI) SendUnAuthorizedError(err error) {
b.RenderFormattedError(http.StatusUnauthorized, err.Error())
}
// SendConflictError sends conflict error to the client.
func (b *BaseAPI) SendConflictError(err error) {
b.RenderFormattedError(http.StatusConflict, err.Error())
}
// SendNotFoundError sends not found error to the client.
func (b *BaseAPI) SendNotFoundError(err error) {
b.RenderFormattedError(http.StatusNotFound, err.Error())
}
// SendBadRequestError sends bad request error to the client.
func (b *BaseAPI) SendBadRequestError(err error) {
b.RenderFormattedError(http.StatusBadRequest, err.Error())
}
// SendInternalServerError sends internal server error to the client.
func (b *BaseAPI) SendInternalServerError(err error) {
b.RenderFormattedError(http.StatusInternalServerError, err.Error())
}
// SendForbiddenError sends forbidden error to the client.
func (b *BaseAPI) SendForbiddenError(err error) {
b.RenderFormattedError(http.StatusForbidden, err.Error())
}
// SendPreconditionFailedError sends forbidden error to the client.
func (b *BaseAPI) SendPreconditionFailedError(err error) {
b.RenderFormattedError(http.StatusPreconditionFailed, err.Error())
}
// SendStatusServiceUnavailableError sends forbidden error to the client.
func (b *BaseAPI) SendStatusServiceUnavailableError(err error) {
b.RenderFormattedError(http.StatusServiceUnavailable, err.Error())
}
......@@ -46,3 +46,13 @@ func (f *FileKeyProvider) Get(params map[string]interface{}) (string, error) {
}
return string(b), nil
}
// PresetKeyProvider returns the preset key disregarding the parm, this is for testing only
type PresetKeyProvider struct {
Key string
}
// Get ...
func (p *PresetKeyProvider) Get(params map[string]interface{}) (string, error) {
return p.Key, nil
}
......@@ -15,6 +15,7 @@
package config
import (
"github.com/stretchr/testify/assert"
"io/ioutil"
"os"
"testing"
......@@ -42,3 +43,12 @@ func TestGetOfFileKeyProvider(t *testing.T) {
return
}
}
func TestPresetKeyProvider(t *testing.T) {
kp := &PresetKeyProvider{
Key: "mykey",
}
k, err := kp.Get(nil)
assert.Nil(t, err)
assert.Equal(t, "mykey", k)
}
......@@ -133,7 +133,7 @@ var (
{Name: common.HTTPAuthProxyEndpoint, Scope: UserScope, Group: HTTPAuthGroup, ItemType: &StringType{}},
{Name: common.HTTPAuthProxyTokenReviewEndpoint, Scope: UserScope, Group: HTTPAuthGroup, ItemType: &StringType{}},
{Name: common.HTTPAuthProxySkipCertVerify, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "false", ItemType: &BoolType{}},
{Name: common.HTTPAuthProxyVerifyCert, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "true", ItemType: &BoolType{}},
{Name: common.HTTPAuthProxyAlwaysOnboard, Scope: UserScope, Group: HTTPAuthGroup, DefaultValue: "false", ItemType: &BoolType{}},
{Name: common.OIDCName, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}},
......@@ -141,7 +141,7 @@ var (
{Name: common.OIDCCLientID, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}},
{Name: common.OIDCClientSecret, Scope: UserScope, Group: OIDCGroup, ItemType: &PasswordType{}},
{Name: common.OIDCScope, Scope: UserScope, Group: OIDCGroup, ItemType: &StringType{}},
{Name: common.OIDCSkipCertVerify, Scope: UserScope, Group: OIDCGroup, DefaultValue: "false", ItemType: &BoolType{}},
{Name: common.OIDCVerifyCert, Scope: UserScope, Group: OIDCGroup, DefaultValue: "true", ItemType: &BoolType{}},
{Name: "with_chartmuseum", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CHARTMUSEUM", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},
{Name: "with_clair", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CLAIR", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},
......
......@@ -100,13 +100,13 @@ const (
UAAVerifyCert = "uaa_verify_cert"
HTTPAuthProxyEndpoint = "http_authproxy_endpoint"
HTTPAuthProxyTokenReviewEndpoint = "http_authproxy_tokenreview_endpoint"
HTTPAuthProxySkipCertVerify = "http_authproxy_skip_cert_verify"
HTTPAuthProxyVerifyCert = "http_authproxy_verify_cert"
HTTPAuthProxyAlwaysOnboard = "http_authproxy_always_onboard"
OIDCName = "oidc_name"
OIDCEndpoint = "oidc_endpoint"
OIDCCLientID = "oidc_client_id"
OIDCClientSecret = "oidc_client_secret"
OIDCSkipCertVerify = "oidc_skip_cert_verify"
OIDCVerifyCert = "oidc_verify_cert"
OIDCScope = "oidc_scope"
DefaultClairEndpoint = "http://clair:6060"
......
......@@ -15,16 +15,26 @@
package http
import (
"encoding/json"
"fmt"
)
// Error wrap HTTP status code and message as an error
type Error struct {
Code int
Message string
Code int `json:"code"`
Message string `json:"message"`
}
// Error ...
func (e *Error) Error() string {
return fmt.Sprintf("http error: code %d, message %s", e.Code, e.Message)
}
// String wraps the error msg to the well formatted error message
func (e *Error) String() string {
data, err := json.Marshal(&e)
if err != nil {
return e.Message
}
return string(data)
}
package http
import (
"github.com/stretchr/testify/assert"
"testing"
)
// Test case for error wrapping function.
func TestWrapError(t *testing.T) {
err := Error{
Code: 1,
Message: "test",
}
assert.Equal(t, err.String(), "{\"code\":1,\"message\":\"test\"}")
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment