Merge branch 'goharbor:master' into fix-basedn-empty

This commit is contained in:
Alexis L 2021-07-09 22:32:35 +02:00 committed by GitHub
commit 2ce814329b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 832 additions and 869 deletions

View File

@ -29,10 +29,10 @@ jobs:
- ubuntu-latest
timeout-minutes: 100
steps:
- name: Set up Go 1.15
- name: Set up Go 1.16
uses: actions/setup-go@v1
with:
go-version: 1.15.12
go-version: 1.16.5
id: go
- name: setup Docker
uses: docker-practice/actions-setup-docker@0.0.1
@ -95,10 +95,10 @@ jobs:
- ubuntu-latest
timeout-minutes: 100
steps:
- name: Set up Go 1.15
- name: Set up Go 1.16
uses: actions/setup-go@v1
with:
go-version: 1.15.12
go-version: 1.16.5
id: go
- name: setup Docker
uses: docker-practice/actions-setup-docker@0.0.1
@ -155,10 +155,10 @@ jobs:
- ubuntu-latest
timeout-minutes: 100
steps:
- name: Set up Go 1.15
- name: Set up Go 1.16
uses: actions/setup-go@v1
with:
go-version: 1.15.12
go-version: 1.16.5
id: go
- name: setup Docker
uses: docker-practice/actions-setup-docker@0.0.1
@ -215,10 +215,10 @@ jobs:
- ubuntu-latest
timeout-minutes: 100
steps:
- name: Set up Go 1.15
- name: Set up Go 1.16
uses: actions/setup-go@v1
with:
go-version: 1.15.12
go-version: 1.16.5
id: go
- name: setup Docker
uses: docker-practice/actions-setup-docker@0.0.1
@ -273,10 +273,10 @@ jobs:
- ubuntu-latest
timeout-minutes: 100
steps:
- name: Set up Go 1.15
- name: Set up Go 1.16
uses: actions/setup-go@v1
with:
go-version: 1.15.12
go-version: 1.16.5
id: go
- name: setup Docker
uses: docker-practice/actions-setup-docker@0.0.1

View File

@ -26,10 +26,10 @@ jobs:
service_account_key: ${{ secrets.GCP_SA_KEY }}
export_default_credentials: true
- run: gcloud info
- name: Set up Go 1.15
- name: Set up Go 1.16
uses: actions/setup-go@v1
with:
go-version: 1.15.12
go-version: 1.16.5
id: go
- name: setup Docker
uses: docker-practice/actions-setup-docker@0.0.1

View File

@ -26,10 +26,10 @@ jobs:
service_account_key: ${{ secrets.GCP_SA_KEY }}
export_default_credentials: true
- run: gcloud info
- name: Set up Go 1.15
- name: Set up Go 1.16
uses: actions/setup-go@v1
with:
go-version: 1.15.12
go-version: 1.16.5
id: go
- name: setup Docker
uses: docker-practice/actions-setup-docker@0.0.1

View File

@ -158,9 +158,10 @@ Harbor backend is written in [Go](http://golang.org/). If you don't have a Harbo
| 1.9 | 1.12.12 |
| 1.10 | 1.12.12 |
| 2.0 | 1.13.15 |
| 2.1 | 1.14.13 |
| 2.1 | 1.14.13 |
| 2.2 | 1.15.6 |
| 2.3 | 1.15.12 |
| 2.3 | 1.15.12 |
| 2.4 | 1.16.5 |
Ensure your GOPATH and PATH have been configured in accordance with the Go environment instructions.

View File

@ -9,7 +9,7 @@
# compile_golangimage:
# compile from golang image
# for example: make compile_golangimage -e GOBUILDIMAGE= \
# golang:1.15.12
# golang:1.16.5
# compile_core, compile_jobservice: compile specific binary
#
# build: build Harbor docker images from photon baseimage
@ -156,7 +156,7 @@ GOINSTALL=$(GOCMD) install
GOTEST=$(GOCMD) test
GODEP=$(GOTEST) -i
GOFMT=gofmt -w
GOBUILDIMAGE=golang:1.15.12
GOBUILDIMAGE=golang:1.16.5
GOBUILDPATHINCONTAINER=/harbor
# go build

View File

@ -2697,6 +2697,11 @@ paths:
- usergroup
parameters:
- $ref: '#/parameters/requestId'
- name: ldap_group_dn
in: query
type: string
required: false
description: search with ldap group DN
responses:
'200':
description: Get user group successfully.
@ -8272,27 +8277,33 @@ definitions:
properties:
value:
type: string
x-omitempty: false
description: The string value of current config item
editable:
type: boolean
x-omitempty: false
description: The configure item can be updated or not
BoolConfigItem:
type: object
properties:
value:
type: boolean
x-omitempty: false
description: The boolean value of current config item
editable:
type: boolean
x-omitempty: false
description: The configure item can be updated or not
IntegerConfigItem:
type: object
properties:
value:
type: integer
x-omitempty: false
description: The integer value of current config item
editable:
type: boolean
x-omitempty: false
description: The configure item can be updated or not
ProjectMemberEntity:
type: object

View File

@ -4,7 +4,7 @@ set +e
usage(){
echo "Usage: builder <golang image:version> <code path> <code release tag> <main.go path> <binary name>"
echo "e.g: builder golang:1.15.12 github.com/helm/chartmuseum v0.12.0 cmd/chartmuseum chartm"
echo "e.g: builder golang:1.16.5 github.com/helm/chartmuseum v0.12.0 cmd/chartmuseum chartm"
exit 1
}

View File

@ -1,4 +1,4 @@
FROM golang:1.15.12
FROM golang:1.14.15
ARG NOTARY_VERSION
ARG MIGRATE_VERSION

View File

@ -1,7 +1,8 @@
FROM golang:1.15.12
FROM golang:1.16.5
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
ENV BUILDTAGS include_oss include_gcs
ENV GO111MODULE auto
WORKDIR $DISTRIBUTION_DIR
COPY . $DISTRIBUTION_DIR

View File

@ -1,4 +1,4 @@
FROM golang:1.15.12
FROM golang:1.16.5
ADD . /go/src/github.com/aquasecurity/harbor-scanner-trivy/
WORKDIR /go/src/github.com/aquasecurity/harbor-scanner-trivy/

View File

@ -19,7 +19,7 @@ TEMP=$(mktemp -d ${TMPDIR-/tmp}/trivy-adapter.XXXXXX)
git clone https://github.com/aquasecurity/harbor-scanner-trivy.git $TEMP
cd $TEMP; git checkout $VERSION; cd -
echo "Building Trivy adapter binary based on golang:1.15.12..."
echo "Building Trivy adapter binary based on golang:1.16.5..."
cp Dockerfile.binary $TEMP
docker build -f $TEMP/Dockerfile.binary -t trivy-adapter-golang $TEMP

View File

@ -1,40 +0,0 @@
package dao
import (
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/models"
"time"
)
// CreateOrUpdateJobLog ...
func CreateOrUpdateJobLog(log *models.JobLog) (int64, error) {
o := GetOrmer()
count, err := o.InsertOrUpdate(log, "job_uuid")
if err != nil {
return 0, err
}
return count, nil
}
// GetJobLog ...
func GetJobLog(uuid string) (*models.JobLog, error) {
o := GetOrmer()
jl := models.JobLog{UUID: uuid}
err := o.Read(&jl, "UUID")
if err == orm.ErrNoRows {
return nil, err
}
return &jl, nil
}
// DeleteJobLogsBefore ...
func DeleteJobLogsBefore(t time.Time) (int64, error) {
o := GetOrmer()
sql := `delete from job_log where creation_time < ?`
res, err := o.Raw(sql, t).Exec()
if err != nil {
return 0, err
}
return res.RowsAffected()
}

View File

@ -1,44 +0,0 @@
package dao
import (
"testing"
"github.com/goharbor/harbor/src/common/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"time"
)
func TestMethodsOfJobLog(t *testing.T) {
uuid := "uuid_for_unit_test"
now := time.Now()
content := "content for unit text"
jobLog := &models.JobLog{
UUID: uuid,
CreationTime: now,
Content: content,
}
// create
_, err := CreateOrUpdateJobLog(jobLog)
require.Nil(t, err)
// update
updateContent := "content for unit text update"
jobLog.Content = updateContent
_, err = CreateOrUpdateJobLog(jobLog)
require.Nil(t, err)
// get
log, err := GetJobLog(uuid)
require.Nil(t, err)
assert.Equal(t, now.Second(), log.CreationTime.Second())
assert.Equal(t, updateContent, log.Content)
assert.Equal(t, jobLog.LogID, log.LogID)
// delete
count, err := DeleteJobLogsBefore(time.Now().Add(time.Duration(time.Minute)))
require.Nil(t, err)
assert.Equal(t, int64(1), count)
}

View File

@ -23,7 +23,6 @@ func init() {
new(User),
new(Role),
new(ResourceLabel),
new(JobLog),
new(OIDCUser),
)
}

View File

@ -1,22 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package error
import (
"errors"
)
// ErrDupProject is the error returned when creating a duplicate project
var ErrDupProject = errors.New("duplicate project")

View File

@ -1,94 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package utils
import (
"strings"
)
// Link : HTTP link header
type Link struct {
// URL : url part of header
URL string
// Rel : prev or next
Rel string
}
// Links : multiple link
type Links []*Link
// Prev returns the URL indicated by "prev" rel.
func (l Links) Prev() string {
prev := ""
for _, link := range l {
if strings.ToLower(link.Rel) == "prev" {
prev = link.URL
break
}
}
return prev
}
// Next returns the URL indicated by "next" rel.
func (l Links) Next() string {
next := ""
for _, link := range l {
if link.Rel == "next" {
next = link.URL
break
}
}
return next
}
// ParseLink parses the raw link header to Links
func ParseLink(raw string) Links {
links := Links{}
for _, l := range strings.Split(raw, ",") {
link := parseSingleLink(l)
if link != nil {
links = append(links, link)
}
}
return links
}
func parseSingleLink(raw string) *Link {
link := &Link{}
for _, str := range strings.Split(raw, ";") {
str = strings.TrimSpace(str)
if strings.HasPrefix(str, "<") && strings.HasSuffix(str, ">") {
str = strings.Trim(str, "<>")
link.URL = str
continue
}
parts := strings.SplitN(str, "=", 2)
if len(parts) != 2 || strings.ToLower(parts[0]) != "rel" {
continue
}
link.Rel = strings.ToLower(strings.Trim(parts[1], "\""))
}
if len(link.URL) == 0 || len(link.Rel) == 0 {
link = nil
}
return link
}

View File

@ -1,61 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package utils
import (
"sync"
"time"
)
var (
scanAllMarker *TimeMarker
scanOverviewMarker = &TimeMarker{
interval: 15 * time.Second,
}
once sync.Once
)
// TimeMarker is used to control an action not to be taken frequently within the interval
type TimeMarker struct {
sync.RWMutex
next time.Time
interval time.Duration
}
// Mark tries to mark a future time, which is after the duration of interval from the time it's called.
func (t *TimeMarker) Mark() {
t.Lock()
defer t.Unlock()
t.next = time.Now().Add(t.interval)
}
// Check returns true if the current time is after the mark by this marker, and the caction the mark guards and be taken.
func (t *TimeMarker) Check() bool {
t.RLock()
defer t.RUnlock()
return time.Now().After(t.next)
}
// Next returns the time of the next mark.
func (t *TimeMarker) Next() time.Time {
t.RLock()
defer t.RUnlock()
return t.next
}
// ScanOverviewMarker ...
func ScanOverviewMarker() *TimeMarker {
return scanOverviewMarker
}

View File

@ -1,37 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package utils
import (
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestTimeMarker(t *testing.T) {
assert := assert.New(t)
m := &TimeMarker{
interval: 1 * time.Second,
}
r1 := m.Check()
assert.True(r1)
m.Mark()
r2 := m.Check()
assert.False(r2)
t.Log("Sleep for 2 seconds...")
time.Sleep(2 * time.Second)
r3 := m.Check()
assert.True(r3)
}

View File

@ -209,16 +209,6 @@ func SafeCastFloat64(value interface{}) float64 {
return 0
}
// ParseOfftime ...
func ParseOfftime(offtime int64) (hour, minite, second int) {
offtime = offtime % (3600 * 24)
hour = int(offtime / 3600)
offtime = offtime % 3600
minite = int(offtime / 60)
second = int(offtime % 60)
return
}
// TrimLower ...
func TrimLower(str string) string {
return strings.TrimSpace(strings.ToLower(str))
@ -268,11 +258,6 @@ func IsContainIllegalChar(s string, illegalChar []string) bool {
return false
}
// IsDigest A sha256 is a string with 64 characters.
func IsDigest(ref string) bool {
return strings.HasPrefix(ref, "sha256:") && len(ref) == 71
}
// ParseJSONInt ...
func ParseJSONInt(value interface{}) (int, bool) {
switch value.(type) {
@ -295,13 +280,3 @@ func FindNamedMatches(regex *regexp.Regexp, str string) map[string]string {
}
return results
}
// ParamPlaceholderForIn returns a string that contains placeholders for sql keyword "in"
// e.g. n=3, returns "?,?,?"
func ParamPlaceholderForIn(n int) string {
placeholders := []string{}
for i := 0; i < n; i++ {
placeholders = append(placeholders, "?")
}
return strings.Join(placeholders, ",")
}

View File

@ -160,43 +160,6 @@ func TestGenerateRandomStringWithLen(t *testing.T) {
}
}
func TestParseLink(t *testing.T) {
raw := ""
links := ParseLink(raw)
if len(links) != 0 {
t.Errorf("unexpected length: %d != %d", len(links), 0)
}
raw = "a;b,c"
links = ParseLink(raw)
if len(links) != 0 {
t.Errorf("unexpected length: %d != %d", len(links), 0)
}
raw = `</api/users?page=1&page_size=100>; rel="prev"`
links = ParseLink(raw)
if len(links) != 1 {
t.Errorf("unexpected length: %d != %d", len(links), 1)
}
prev := `/api/users?page=1&page_size=100`
if links.Prev() != prev {
t.Errorf("unexpected prev: %s != %s", links.Prev(), prev)
}
raw = `</api/users?page=1&page_size=100>; rel="prev", </api/users?page=3&page_size=100>; rel="next"`
links = ParseLink(raw)
if len(links) != 2 {
t.Errorf("unexpected length: %d != %d", len(links), 2)
}
prev = `/api/users?page=1&page_size=100`
if links.Prev() != prev {
t.Errorf("unexpected prev: %s != %s", links.Prev(), prev)
}
next := `/api/users?page=3&page_size=100`
if links.Next() != next {
t.Errorf("unexpected prev: %s != %s", links.Next(), next)
}
}
func TestTestTCPConn(t *testing.T) {
server := httptest.NewServer(nil)
defer server.Close()
@ -354,29 +317,6 @@ func TestSafeCastFloat64(t *testing.T) {
}
}
func TestParseOfftime(t *testing.T) {
cases := []struct {
offtime int64
hour int
minite int
second int
}{
{0, 0, 0, 0},
{1, 0, 0, 1},
{60, 0, 1, 0},
{3600, 1, 0, 0},
{3661, 1, 1, 1},
{3600*24 + 60, 0, 1, 0},
}
for _, c := range cases {
h, m, s := ParseOfftime(c.offtime)
assert.Equal(t, c.hour, h)
assert.Equal(t, c.minite, m)
assert.Equal(t, c.second, s)
}
}
func TestTrimLower(t *testing.T) {
type args struct {
str string
@ -426,9 +366,3 @@ func TestGetStrValueOfAnyType(t *testing.T) {
})
}
}
func TestIsDigest(t *testing.T) {
assert := assert.New(t)
assert.False(IsDigest("latest"))
assert.True(IsDigest("sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7"))
}

View File

@ -1,24 +0,0 @@
package utils
import (
"fmt"
"regexp"
)
const nameComponent = `[a-z0-9]+((?:[._]|__|[-]*)[a-z0-9]+)*`
// TagRegexp is regular expression to match image tags, for example, 'v1.0'
var TagRegexp = regexp.MustCompile(`^[\w][\w.-]{0,127}$`)
// RepoRegexp is regular expression to match repo name, for example, 'busybox', 'stage/busybox'
var RepoRegexp = regexp.MustCompile(fmt.Sprintf("^%s(/%s)*$", nameComponent, nameComponent))
// ValidateTag validates whether a tag is valid.
func ValidateTag(tag string) bool {
return TagRegexp.MatchString(tag)
}
// ValidateRepo validates whether a repo name is valid.
func ValidateRepo(repo string) bool {
return RepoRegexp.MatchString(repo)
}

View File

@ -1,183 +0,0 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidateTag(t *testing.T) {
cases := []struct {
tag string
valid bool
}{
{
"v1.0",
true,
},
{
"1.0.0",
true,
},
{
"v1.0-alpha.0",
true,
},
{
"1__",
true,
},
{
"__v1.0",
true,
},
{
"_...",
true,
},
{
"_-_",
true,
},
{
"--v1.0",
false,
},
{
".0.1",
false,
},
{
"-0.1",
false,
},
{
"0.1.*",
false,
},
{
"0.1.?",
false,
},
}
for _, c := range cases {
if c.valid {
assert.True(t, ValidateTag(c.tag))
} else {
assert.False(t, ValidateTag(c.tag))
}
}
}
func TestValidateRepo(t *testing.T) {
cases := []struct {
repo string
valid bool
}{
{
"a",
true,
},
{
"a_a",
true,
},
{
"a__a",
true,
},
{
"a-a",
true,
},
{
"a--a",
true,
},
{
"a---a",
true,
},
{
"a.a",
true,
},
{
"a/b.b",
true,
},
{
"a_a/b-b",
true,
},
{
".a",
false,
},
{
"_a",
false,
},
{
"-a",
false,
},
{
"a.",
false,
},
{
"a_",
false,
},
{
"a-",
false,
},
{
"a..a",
false,
},
{
"a___a",
false,
},
{
"a.-a",
false,
},
{
"a_-a",
false,
},
{
"a*",
false,
},
{
"A/_a",
false,
},
{
"A/.a",
false,
},
{
"Aaaa",
false,
},
{
"aaaA",
false,
},
}
for _, c := range cases {
if c.valid {
assert.True(t, ValidateRepo(c.repo))
} else {
assert.False(t, ValidateRepo(c.repo))
}
}
}

View File

@ -1,6 +1,6 @@
module github.com/goharbor/harbor/src
go 1.15
go 1.16
require (
github.com/Azure/azure-sdk-for-go v37.2.0+incompatible // indirect

View File

@ -15,6 +15,7 @@
package gc
import (
"github.com/goharbor/harbor/src/lib"
"os"
"time"
@ -270,9 +271,13 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
}
// for manifest, it has to delete the revisions folder of each repository
gc.logger.Infof("delete manifest from storage: %s", blob.Digest)
if err := ignoreNotFound(func() error {
return gc.registryCtlClient.DeleteManifest(art.RepositoryName, blob.Digest)
}); err != nil {
if err := lib.RetryUntil(func() error {
return ignoreNotFound(func() error {
return gc.registryCtlClient.DeleteManifest(art.RepositoryName, blob.Digest)
})
}, lib.RetryCallback(func(err error, sleep time.Duration) {
gc.logger.Infof("failed to exec DeleteManifest retry after %s : %v", sleep, err)
})); err != nil {
if err := ignoreNotFound(func() error {
return gc.markDeleteFailed(ctx, blob)
}); err != nil {
@ -294,9 +299,13 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
// for the foreign layer, as it's not stored in the storage, no need to call the delete api and count size, but still have to delete the DB record.
if !blob.IsForeignLayer() {
gc.logger.Infof("delete blob from storage: %s", blob.Digest)
if err := ignoreNotFound(func() error {
return gc.registryCtlClient.DeleteBlob(blob.Digest)
}); err != nil {
if err := lib.RetryUntil(func() error {
return ignoreNotFound(func() error {
return gc.registryCtlClient.DeleteBlob(blob.Digest)
})
}, lib.RetryCallback(func(err error, sleep time.Duration) {
gc.logger.Infof("failed to exec DeleteBlob retry after %s : %v", sleep, err)
})); err != nil {
if err := ignoreNotFound(func() error {
return gc.markDeleteFailed(ctx, blob)
}); err != nil {

View File

@ -2,9 +2,11 @@ package gc
import (
"fmt"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/registry"
"github.com/gomodule/redigo/redis"
"time"
)
// delKeys ...
@ -50,10 +52,11 @@ func v2DeleteManifest(repository, digest string) error {
if !exist {
return nil
}
if err := registry.Cli.DeleteManifest(repository, digest); err != nil {
return err
}
return nil
return lib.RetryUntil(func() error {
return registry.Cli.DeleteManifest(repository, digest)
}, lib.RetryCallback(func(err error, sleep time.Duration) {
fmt.Printf("failed to exec DeleteManifest retry after %s : %v\n", sleep, err)
}))
}
// ignoreNotFound ignores the NotFoundErr error

View File

@ -3,9 +3,10 @@ package backend
import (
"bufio"
"bytes"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/joblog"
"github.com/goharbor/harbor/src/pkg/joblog/models"
)
// DBLogger is an implementation of logger.Interface.
@ -47,7 +48,7 @@ func (dbl *DBLogger) Close() error {
Content: dbl.buffer.String(),
}
_, err = dao.CreateOrUpdateJobLog(&jobLog)
_, err = joblog.Mgr.Create(orm.Context(), &jobLog)
if err != nil {
return err
}

View File

@ -3,8 +3,9 @@ package getter
import (
"errors"
"fmt"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/joblog"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/jobservice/errs"
)
@ -23,7 +24,7 @@ func (dbg *DBGetter) Retrieve(logID string) ([]byte, error) {
return nil, errors.New("empty log identify")
}
jobLog, err := dao.GetJobLog(logID)
jobLog, err := joblog.Mgr.Get(orm.Context(), logID)
if err != nil {
// Other errors have been ignored by GetJobLog()
return nil, errs.NoObjectFoundError(fmt.Sprintf("log entity: %s", logID))

View File

@ -2,7 +2,8 @@ package sweeper
import (
"fmt"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/joblog"
"time"
)
@ -28,7 +29,7 @@ func (dbs *DBSweeper) Sweep() (int, error) {
// Start to sweep logs
before := time.Now().Add(time.Duration(dbs.duration) * oneDay * -1)
count, err := dao.DeleteJobLogsBefore(before)
count, err := joblog.Mgr.DeleteBefore(orm.Context(), before)
if err != nil {
return 0, fmt.Errorf("sweep logs in DB failed before %s with error: %s", before, err)

View File

@ -19,7 +19,7 @@ const (
var (
// V2ManifestURLRe is the regular expression for matching request v2 handler to view/delete manifest
V2ManifestURLRe = regexp.MustCompile(fmt.Sprintf(`^/v2/(?P<%s>%s)/manifests/(?P<%s>%s|%s)$`, RepositorySubexp, reference.NameRegexp.String(), ReferenceSubexp, reference.TagRegexp.String(), digest.DigestRegexp.String()))
V2ManifestURLRe = regexp.MustCompile(fmt.Sprintf(`^/v2/(?P<%s>%s)/manifests/(?P<%s>.*)$`, RepositorySubexp, reference.NameRegexp.String(), ReferenceSubexp))
// V2TagListURLRe is the regular expression for matching request to v2 handler to list tags
V2TagListURLRe = regexp.MustCompile(fmt.Sprintf(`^/v2/(?P<%s>%s)/tags/list`, RepositorySubexp, reference.NameRegexp.String()))
// V2BlobURLRe is the regular expression for matching request to v2 handler to retrieve head/delete a blob

View File

@ -21,12 +21,18 @@ import (
)
func TestMatchManifestURLPattern(t *testing.T) {
_, _, ok := MatchManifestURLPattern("")
assert.False(t, ok)
_, _, ok := MatchManifestURLPattern("/v2/library/hello-world/manifests/.Invalid")
assert.True(t, ok)
_, _, ok = MatchManifestURLPattern("/v2/")
assert.False(t, ok)
_, _, ok = MatchManifestURLPattern("/v2/library/hello-world/manifests//")
assert.True(t, ok)
_, _, ok = MatchManifestURLPattern("/v2/library/hello-world/manifests/###")
assert.True(t, ok)
repository, reference, ok := MatchManifestURLPattern("/v2/library/hello-world/manifests/latest")
assert.True(t, ok)
assert.Equal(t, "library/hello-world", repository)

69
src/pkg/joblog/dao/dao.go Normal file
View File

@ -0,0 +1,69 @@
package dao
import (
"context"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/joblog/models"
"time"
)
// DAO is the data access object for job log
type DAO interface {
// Create the job log
Create(ctx context.Context, jobLog *models.JobLog) (id int64, err error)
// Get the job log specified by UUID
Get(ctx context.Context, uuid string) (jobLog *models.JobLog, err error)
// DeleteBefore the job log specified by time
DeleteBefore(ctx context.Context, t time.Time) (id int64, err error)
}
// New returns an instance of the default DAO
func New() DAO {
return &dao{}
}
type dao struct{}
// Create ...
func (d *dao) Create(ctx context.Context, jobLog *models.JobLog) (int64, error) {
ormer, err := orm.FromContext(ctx)
if err != nil {
return 0, err
}
count, err := ormer.InsertOrUpdate(jobLog, "job_uuid")
if err != nil {
return 0, err
}
return count, nil
}
// Get ...
func (d *dao) Get(ctx context.Context, uuid string) (jobLog *models.JobLog, err error) {
ormer, err := orm.FromContext(ctx)
if err != nil {
return nil, err
}
jl := models.JobLog{UUID: uuid}
err = ormer.Read(&jl, "UUID")
if e := orm.AsNotFoundError(err, "no job log founded"); e != nil {
log.Warningf("no job log founded. Query condition, uuid: %s, err: %v", uuid, e)
return nil, err
}
return &jl, nil
}
// DeleteBefore ...
func (d *dao) DeleteBefore(ctx context.Context, t time.Time) (id int64, err error) {
ormer, err := orm.FromContext(ctx)
if err != nil {
return 0, err
}
sql := `delete from job_log where creation_time < ?`
res, err := ormer.Raw(sql, t).Exec()
if err != nil {
return 0, err
}
return res.RowsAffected()
}

View File

@ -0,0 +1,53 @@
package dao
import (
"github.com/goharbor/harbor/src/pkg/joblog/models"
htesting "github.com/goharbor/harbor/src/testing"
"time"
)
type DaoTestSuite struct {
htesting.Suite
dao DAO
}
func (suite *DaoTestSuite) SetupSuite() {
suite.Suite.SetupSuite()
suite.Suite.ClearTables = []string{"job_log"}
suite.dao = New()
}
func (suite *DaoTestSuite) TestMethodsOfJobLog() {
ctx := suite.Context()
uuid := "uuid_for_unit_test"
now := time.Now()
content := "content for unit text"
jobLog := &models.JobLog{
UUID: uuid,
CreationTime: now,
Content: content,
}
// create
_, err := suite.dao.Create(ctx, jobLog)
suite.Nil(err)
// update
updateContent := "content for unit text update"
jobLog.Content = updateContent
_, err = suite.dao.Create(ctx, jobLog)
suite.Nil(err)
// get
log, err := suite.dao.Get(ctx, uuid)
suite.Nil(err)
suite.Equal(now.Second(), log.CreationTime.Second())
suite.Equal(updateContent, log.Content)
suite.Equal(jobLog.LogID, log.LogID)
// delete
count, err := suite.dao.DeleteBefore(ctx, time.Now().Add(time.Duration(time.Minute)))
suite.Nil(err)
suite.Equal(int64(1), count)
}

47
src/pkg/joblog/manager.go Normal file
View File

@ -0,0 +1,47 @@
package joblog
import (
"context"
"github.com/goharbor/harbor/src/pkg/joblog/dao"
"github.com/goharbor/harbor/src/pkg/joblog/models"
"time"
)
// Mgr is the global job log manager instance
var Mgr = New()
// Manager is used for job log management
type Manager interface {
// Get the job log specified by ID
Get(ctx context.Context, uuid string) (jobLog *models.JobLog, err error)
// Create the job log
Create(ctx context.Context, jobLog *models.JobLog) (id int64, err error)
// DeleteBefore the job log specified by time
DeleteBefore(ctx context.Context, t time.Time) (id int64, err error)
}
// New returns a default implementation of Manager
func New() Manager {
return &manager{
dao: dao.New(),
}
}
type manager struct {
dao dao.DAO
}
// Get ...
func (m *manager) Get(ctx context.Context, uuid string) (jobLog *models.JobLog, err error) {
return m.dao.Get(ctx, uuid)
}
// Create ...
func (m *manager) Create(ctx context.Context, jobLog *models.JobLog) (id int64, err error) {
return m.dao.Create(ctx, jobLog)
}
// DeleteJobLogsBefore ...
func (m *manager) DeleteBefore(ctx context.Context, t time.Time) (id int64, err error) {
return m.dao.DeleteBefore(ctx, t)
}

View File

@ -0,0 +1,76 @@
package joblog
import (
"context"
"github.com/goharbor/harbor/src/pkg/joblog/models"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/goharbor/harbor/src/testing/pkg/joblog/dao"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"os"
"testing"
)
type managerTestingSuite struct {
suite.Suite
t *testing.T
assert *assert.Assertions
require *require.Assertions
mockJobLogDao *dao.DAO
}
func (m *managerTestingSuite) SetupSuite() {
m.t = m.T()
m.assert = assert.New(m.t)
m.require = require.New(m.t)
err := os.Setenv("RUN_MODE", "TEST")
m.require.Nil(err)
}
func (m *managerTestingSuite) TearDownSuite() {
err := os.Unsetenv("RUN_MODE")
m.require.Nil(err)
}
func (m *managerTestingSuite) SetupTest() {
m.mockJobLogDao = &dao.DAO{}
Mgr = &manager{
dao: m.mockJobLogDao,
}
}
func TestManagerTestingSuite(t *testing.T) {
suite.Run(t, &managerTestingSuite{})
}
func (m *managerTestingSuite) TestCreate() {
m.mockJobLogDao.On("Create", mock.Anything, mock.Anything).Return(int64(1), nil)
id, err := Mgr.Create(context.Background(), &models.JobLog{})
m.mockJobLogDao.AssertCalled(m.t, "Create", mock.Anything, mock.Anything)
m.require.Nil(err)
m.assert.Equal(int64(1), id)
}
func (m *managerTestingSuite) TestGet() {
m.mockJobLogDao.On("Get", mock.Anything, mock.Anything).Return(&models.JobLog{
LogID: 1,
UUID: "1234",
Content: "test get",
}, nil)
ir, err := Mgr.Get(context.Background(), "1234")
m.mockJobLogDao.AssertCalled(m.t, "Get", mock.Anything, mock.Anything)
m.require.Nil(err)
m.require.NotNil(ir)
m.assert.Equal(1, ir.LogID)
}
func (m *managerTestingSuite) TestDeleteBefore() {
m.mockJobLogDao.On("DeleteBefore", mock.Anything, mock.Anything).Return(int64(1), nil)
_, err := Mgr.DeleteBefore(context.Background(), time.Now())
m.mockJobLogDao.AssertCalled(m.t, "DeleteBefore", mock.Anything, mock.Anything)
m.require.Nil(err)
}

View File

@ -1,9 +1,14 @@
package models
import (
"github.com/astaxie/beego/orm"
"time"
)
func init() {
orm.RegisterModel(&JobLog{})
}
// JobLogTable is the name of the table that record the job execution result.
const JobLogTable = "job_log"

View File

@ -30,7 +30,7 @@ func init() {
)
}
// RepoRecord holds the record of an repository in DB, all the infors are from the registry notification event.
// RepoRecord holds the record of an repository in DB, all the infos are from the registry notification event.
type RepoRecord struct {
RepositoryID int64 `orm:"pk;auto;column(repository_id)" json:"repository_id"`
Name string `orm:"column(name)" json:"name"`
@ -57,7 +57,7 @@ func (r *RepoRecord) FilterByBlobDigest(ctx context.Context, qs orm.QuerySeter,
return qs.FilterRaw("repository_id", fmt.Sprintf("in (%s)", sql))
}
// TableName is required by by beego orm to map RepoRecord to table repository
// TableName is required by beego orm to map RepoRecord to table repository
func (r *RepoRecord) TableName() string {
return "repository"
}

View File

@ -2,55 +2,80 @@
<global-message [isAppLevel]="true"></global-message>
<navigator (showDialogModalAction)="openModal($event)"></navigator>
<search-result></search-result>
<div class="login-wrapper" [ngStyle]="{'background-image': customLoginBgImg? 'url(/images/' + customLoginBgImg + ')': ''}">
<div class="login-wrapper"
[ngStyle]="{'background-image': customLoginBgImg? 'url(/images/' + customLoginBgImg + ')': ''}">
<form #signInForm="ngForm" class="login">
<label class="title"> {{customAppTitle? customAppTitle:(appTitle | translate)}}
<span *ngIf="isOidcLoginMode && steps===2" (click)="steps=1" class="back-icon">
<clr-icon shape="angle left"></clr-icon>
<div class="margin-left-3">{{'SEARCH.BACK' | translate }}</div>
</span>
<label class="title"> {{customAppTitle ? customAppTitle : (appTitle | translate)}}
</label>
<a href="/c/oidc/login" class="login-oidc" *ngIf="isOidcLoginMode">
<button type="button" id="log_oidc" class="btn btn-primary btn-block">
<span>{{'BUTTON.LOG_IN_OIDC' | translate }}</span>
</button>
</a>
<div class="login-group">
<clr-input-container>
<input clrInput class="username" type="text" required [(ngModel)]="signInCredential.principal" name="login_username" id="login_username"
placeholder='{{"PLACEHOLDER.SIGN_IN_NAME" | translate}}' #userNameInput='ngModel'>
<clr-control-error>{{ 'TOOLTIP.SIGN_IN_USERNAME' | translate }}</clr-control-error>
</clr-input-container>
<div class="clr-form-control">
<div class="clr-control-container" [class.clr-error] ="passwordInput.invalid && (passwordInput.dirty || passwordInput.touched)">
<div class="clr-input-wrapper">
<input class="clr-input pwd-input" [type]="showPwd?'text':'password'" required [(ngModel)]="signInCredential.password" name="login_password" id="login_password"
placeholder='{{"PLACEHOLDER.SIGN_IN_PWD" | translate}}' #passwordInput="ngModel">
<clr-icon *ngIf="!showPwd" shape="eye" class="pw-eye" (click)="showPwd =!showPwd"></clr-icon>
<clr-icon *ngIf="showPwd" shape="eye-hide" class="pw-eye" (click)="showPwd =!showPwd"></clr-icon>
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
<clr-control-error *ngIf="passwordInput.invalid && (passwordInput.dirty || passwordInput.touched)">
{{ 'TOOLTIP.SIGN_IN_PWD' | translate }}
</clr-control-error>
<ng-container *ngIf="isOidcLoginMode && steps ===1">
<a href="/c/oidc/login">
<button type="button" id="log_oidc" class="btn btn-primary btn-block">
<span>{{'BUTTON.LOG_IN_OIDC' | translate }}</span>
</button>
</a>
<div class="divider-container mt-1 mb-1">
<hr class="divider" size="1" noshade/>
<h4 class="m-0"><span>{{'SIGN_IN.OR' | translate }}</span></h4>
<hr class="divider" size="1" noshade/>
</div>
<a href="javascript:void(0)" (click)="steps=2">
<button type="button" id="login-db" class="btn btn-primary btn-block mt-0">
<span>{{'SIGN_IN.VIA_LOCAL_DB' | translate }}</span>
</button>
</a>
</ng-container>
<ng-container *ngIf="(!isOidcLoginMode && steps === 1) || (isOidcLoginMode && steps ===2)">
<clr-input-container class="mt-3">
<input clrInput class="username" type="text" required [(ngModel)]="signInCredential.principal"
name="login_username" id="login_username"
placeholder='{{"PLACEHOLDER.SIGN_IN_NAME" | translate}}' #userNameInput='ngModel'>
<clr-control-error>{{ 'TOOLTIP.SIGN_IN_USERNAME' | translate }}</clr-control-error>
</clr-input-container>
<div class="clr-form-control">
<div class="clr-control-container"
[class.clr-error]="passwordInput.invalid && (passwordInput.dirty || passwordInput.touched)">
<div class="clr-input-wrapper">
<input class="clr-input pwd-input" [type]="showPwd?'text':'password'" required
[(ngModel)]="signInCredential.password" name="login_password" id="login_password"
placeholder='{{"PLACEHOLDER.SIGN_IN_PWD" | translate}}' #passwordInput="ngModel">
<clr-icon *ngIf="!showPwd" shape="eye" class="pw-eye"
(click)="showPwd =!showPwd"></clr-icon>
<clr-icon *ngIf="showPwd" shape="eye-hide" class="pw-eye"
(click)="showPwd =!showPwd"></clr-icon>
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
<clr-control-error
*ngIf="passwordInput.invalid && (passwordInput.dirty || passwordInput.touched)">
{{ 'TOOLTIP.SIGN_IN_PWD' | translate }}
</clr-control-error>
</div>
</div>
<div class="display-flex">
<clr-checkbox-wrapper>
<input clrCheckbox type="checkbox" id="rememberme" #rememberMeBox
(click)="clickRememberMe($event)" [checked]="rememberMe">
<label class="reset-label" for="rememberme">{{ 'SIGN_IN.REMEMBER' | translate }}</label>
</clr-checkbox-wrapper>
</div>
<div *ngIf="isError" class="error active">
<span *ngIf="isCoreServiceAvailable">{{ 'SIGN_IN.INVALID_MSG' | translate }}</span>
<span *ngIf="!isCoreServiceAvailable">{{ 'SIGN_IN.CORE_SERVICE_NOT_AVAILABLE' | translate }}</span>
</div>
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary mt-2" (click)="signIn()"
id="log_in">{{ 'BUTTON.LOG_IN' | translate }}</button>
<a href="javascript:void(0)" class="signup" (click)="signUp()"
*ngIf="selfSignUp">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
</ng-container>
<div class="more-info" [ngClass]="{'pt-1': (!isOidcLoginMode && steps === 1) || (isOidcLoginMode && steps ===2)}">
<a href="https://github.com/goharbor/harbor" target="_blank">{{ 'BUTTON.MORE_INFO' | translate }}</a>
</div>
<div class="display-flex">
<clr-checkbox-wrapper>
<input clrCheckbox type="checkbox" id="rememberme" #rememberMeBox (click)="clickRememberMe($event)" [checked]="rememberMe">
<label class="reset-label" for="rememberme">{{ 'SIGN_IN.REMEMBER' | translate }}</label>
</clr-checkbox-wrapper>
</div>
<div [class.visibility-hidden]="!isError" class="error active">
<span *ngIf="isCoreServiceAvailable">{{ 'SIGN_IN.INVALID_MSG' | translate }}</span>
<span *ngIf="!isCoreServiceAvailable">{{ 'SIGN_IN.CORE_SERVICE_NOT_AVAILABLE' | translate }}</span>
</div>
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary" (click)="signIn()" id="log_in">{{ 'BUTTON.LOG_IN' | translate }}</button>
<a href="javascript:void(0)" class="signup" (click)="signUp()" *ngIf="selfSignUp">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
</div>
<div>
<a href="https://github.com/goharbor/harbor" target="_blank" class="more-info-link">{{ 'BUTTON.MORE_INFO' | translate }}</a>
</div>
</form>
<div *ngIf="appConfig.show_popular_repo" id="pop_repo" class="popular-repo-wrapper">
<top-repo class="repo-container"></top-repo>
</div>
</div>
</clr-main-container>
<sign-up #signupDialog (userCreation)="handleUserCreation($event)"></sign-up>

View File

@ -14,31 +14,6 @@
top: -5px;
}
.popular-repo-wrapper {
background-color: white;
min-height: 100vh;
display: flex;
flex-grow: 1;
margin-top: 24px;
justify-content: center;
flex-direction: column;
height: auto;
position: relative;
border-left: 1px solid #eee;
}
.repo-container {
width: 100%;
margin-top: -210px;
}
.more-info-link {
position: relative;
top: 80px;
left: 294px;
padding-right: 36px;
}
.tm-font {
font-size: 14px !important;
position: relative;
@ -59,10 +34,6 @@
background:transparent;
}
}
.login-oidc{
margin-top: 50px;
}
.reset-label {
font-size:inherit;
color:inherit;
@ -81,4 +52,42 @@
}
.pwd-input {
padding-right: 26px;
}
}
.divider-container {
display: flex;
align-items: center;
justify-content: space-between;
}
.divider {
margin: 0;
width: 8.5rem;
}
.login {
padding-bottom: 180px;
}
.back-icon {
position: absolute;
top: 1rem;
display: flex;
align-items: center;
color: #007cbb;
font-size: 16px;
cursor: pointer;
}
.margin-left-3 {
margin-left: 3px;
}
.title {
position: absolute;
top: 10rem
}
.login-group {
width: 70%;
position: absolute;
top: 10rem
}
.more-info {
text-align: right;
padding-top: 2rem;
}

View File

@ -28,6 +28,7 @@ import { AboutDialogComponent } from "../../shared/components/about-dialog/about
import { CommonRoutes, CONFIG_AUTH_MODE } from "../../shared/entities/shared.const";
import { SignInCredential } from "./sign-in-credential";
import { UserPermissionService } from "../../shared/services";
import {finalize} from "rxjs/operators";
// Define status flags for signing in states
export const signInStatusNormal = 0;
@ -45,7 +46,6 @@ const expireDays = 10;
export class SignInComponent implements AfterViewChecked, OnInit {
showPwd: boolean = false;
redirectUrl: string = "";
appConfig: AppConfig = new AppConfig();
// Remeber me indicator
rememberMe: boolean = false;
rememberedName: string = "";
@ -68,6 +68,8 @@ export class SignInComponent implements AfterViewChecked, OnInit {
password: ""
};
isCoreServiceAvailable: boolean = true;
steps: number = 1;
hasLoadedAppConfig: boolean = false;
constructor(
private router: Router,
private session: SessionService,
@ -88,15 +90,6 @@ export class SignInComponent implements AfterViewChecked, OnInit {
this.customAppTitle = customSkinObj.loginTitle;
}
}
// Before login: Make sure the updated configuration can be loaded
this.appConfigService.load()
.subscribe(updatedConfig => this.appConfig = updatedConfig
, error => {
// Catch the error
console.error("Failed to load bootstrap options with error: ", error);
});
this.route.queryParams
.subscribe(params => {
this.redirectUrl = params["redirect_url"] || "";
@ -117,7 +110,7 @@ export class SignInComponent implements AfterViewChecked, OnInit {
// App title
public get appTitle(): string {
if (this.appConfig && this.appConfig.with_admiral) {
if (this.appConfigService.getConfig() && this.appConfigService.getConfig().with_admiral) {
return "APP_TITLE.VIC";
}
@ -140,11 +133,11 @@ export class SignInComponent implements AfterViewChecked, OnInit {
// Whether show the 'sign up' link
public get selfSignUp(): boolean {
return this.appConfig.auth_mode === CONFIG_AUTH_MODE.DB_AUTH
&& this.appConfig.self_registration;
return this.appConfigService.getConfig() && this.appConfigService.getConfig().auth_mode === CONFIG_AUTH_MODE.DB_AUTH
&& this.appConfigService.getConfig().self_registration;
}
public get isOidcLoginMode(): boolean {
return this.appConfig.auth_mode === CONFIG_AUTH_MODE.OIDC_AUTH;
return this.appConfigService.getConfig() && this.appConfigService.getConfig().auth_mode === CONFIG_AUTH_MODE.OIDC_AUTH;
}
clickRememberMe($event: any): void {
if ($event && $event.target) {
@ -261,11 +254,7 @@ export class SignInComponent implements AfterViewChecked, OnInit {
// after login successfully: Make sure the updated configuration can be loaded
this.appConfigService.load()
.subscribe(updatedConfig => this.appConfig = updatedConfig
, error => {
// Catch the error
console.error("Failed to load bootstrap options with error: ", error);
});
.subscribe();
}, error => {
// 403 oidc login no body;
if (this.isOidcLoginMode && error && error.status === 403) {

View File

@ -73,36 +73,25 @@ export class AppComponent {
});
}
initLanguage() {
/**
* due to the bug(https://github.com/ngx-translate/core/issues/1258) of translate module
* we have to call use method for all supported languages
* use method will load related language json from backend server
*/
const usedLangs: Array<Observable<any>> = [];
supportedLangs.forEach(lang => {
usedLangs.push(this.translate.use(lang));
});
forkJoin(usedLangs).subscribe(() => { // use target lang after all langs json loaded
this.translate.addLangs(supportedLangs);
this.translate.setDefaultLang(DeFaultLang);
let selectedLang: string = DeFaultLang;
if (localStorage && localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY)) {// If user has selected lang, then directly use it
selectedLang = localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY);
} else {// If user has not selected lang, then use browser language(if contained in supportedLangs)
const browserCultureLang: string = this.translate
.getBrowserCultureLang()
.toLowerCase();
if (browserCultureLang && browserCultureLang.trim() !== "") {
if (supportedLangs && supportedLangs.length > 0) {
if (supportedLangs.find(lang => lang === browserCultureLang)) {
selectedLang = browserCultureLang;
}
this.translate.addLangs(supportedLangs);
this.translate.setDefaultLang(DeFaultLang);
let selectedLang: string = DeFaultLang;
if (localStorage && localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY)) {// If user has selected lang, then directly use it
selectedLang = localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY);
} else {// If user has not selected lang, then use browser language(if contained in supportedLangs)
const browserCultureLang: string = this.translate
.getBrowserCultureLang()
.toLowerCase();
if (browserCultureLang && browserCultureLang.trim() !== "") {
if (supportedLangs && supportedLangs.length > 0) {
if (supportedLangs.find(lang => lang === browserCultureLang)) {
selectedLang = browserCultureLang;
}
}
}
localStorage.setItem(DEFAULT_LANG_LOCALSTORAGE_KEY, selectedLang);
this.translate.use(selectedLang);
}
);
}
localStorage.setItem(DEFAULT_LANG_LOCALSTORAGE_KEY, selectedLang);
// use method will load related language json from backend server
this.translate.use(selectedLang);
}
}

View File

@ -149,7 +149,9 @@ export class NavigatorComponent implements OnInit {
switchLanguage(lang: string): void {
this.selectedLang = lang;
localStorage.setItem(DEFAULT_LANG_LOCALSTORAGE_KEY, lang);
this.translate.use(lang);
// due to the bug(https://github.com/ngx-translate/core/issues/1258) of translate module
// have to reload
this.translate.use(lang).subscribe(() => window.location.reload());
}
// Handle the home action

View File

@ -13,6 +13,12 @@ export class OperateInfo {
}
}
export interface OperateInfosLocalstorage {
updated: number; // millisecond
data: OperateInfo[];
newMessageCount: number;
}
export function operateChanges(list: OperateInfo, state?: string, errorInfo?: string, timeStamp?: 0) {
list.state = state;
list.data.errorInf = errorInfo;

View File

@ -65,9 +65,26 @@ describe('OperationComponent', () => {
expect(errorSpan.style.display).toEqual('none');
});
it('check calculateTime function', () => {
expect(component.calculateTime(1000)).toEqual('less than 1 minute');
expect(component.calculateTime(61000)).toEqual('1 minute(s) ago');
expect(component.calculateTime(3601000)).toEqual('1 hour(s) ago');
expect(component.calculateTime(24 * 3601000)).toEqual('1 day(s) ago');
expect(component.calculateTime(
1000,
'less than 1 minute',
' minute(s) ago',
' hour(s) ago',
' day(s) ago')).toEqual('less than 1 minute');
expect(component.calculateTime(61000,
'less than 1 minute',
' minute(s) ago',
' hour(s) ago',
' day(s) ago')).toEqual('1 minute(s) ago');
expect(component.calculateTime(3601000,
'less than 1 minute',
' minute(s) ago',
' hour(s) ago',
' day(s) ago')).toEqual('1 hour(s) ago');
expect(component.calculateTime(24 * 3601000,
'less than 1 minute',
' minute(s) ago',
' hour(s) ago',
' day(s) ago')).toEqual('1 day(s) ago');
});
});

View File

@ -1,7 +1,7 @@
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { OperationService } from "./operation.service";
import { Subscription } from "rxjs";
import { OperateInfo, OperationState } from "./operate";
import { forkJoin, Subscription } from "rxjs";
import { OperateInfo, OperateInfosLocalstorage, OperationState } from "./operate";
import { SlideInOutAnimation } from "../../_animations/slide-in-out.animation";
import { TranslateService } from "@ngx-translate/core";
import { SessionService } from "../../services/session.service";
@ -26,12 +26,11 @@ export class OperationComponent implements OnInit, OnDestroy {
@HostListener('window:beforeunload', ['$event'])
beforeUnloadHander(event) {
if (this.session.getCurrentUser()) {
// storage to localStorage
const timp = new Date().getTime();
// store into localStorage
// group by user id
localStorage.setItem(`${OPERATION_KEY}-${this.session.getCurrentUser().user_id}`,
JSON.stringify({
timp: timp,
updated: new Date().getTime(),
data: this.resultLists,
newMessageCount: this._newMessageCount
}));
@ -107,28 +106,27 @@ export class OperationComponent implements OnInit, OnDestroy {
init() {
if (this.session.getCurrentUser()) {
let requestCookie = localStorage.getItem(`${OPERATION_KEY}-${this.session.getCurrentUser().user_id}`);
if (requestCookie) {
let operInfors: any = JSON.parse(requestCookie);
if (operInfors) {
if (operInfors.newMessageCount) {
this._newMessageCount = operInfors.newMessageCount;
const operationInfosString: string = localStorage.getItem(`${OPERATION_KEY}-${this.session.getCurrentUser().user_id}`);
if (operationInfosString) {
const operationInfos: OperateInfosLocalstorage = JSON.parse(operationInfosString);
if (operationInfos) {
if (operationInfos.newMessageCount) {
this._newMessageCount = operationInfos.newMessageCount;
}
if ((new Date().getTime() - operInfors.timp) > MAX_SAVING_TIME) {
localStorage.removeItem(`${OPERATION_KEY}-${this.session.getCurrentUser().user_id}`);
} else {
if (operInfors.data) {
operInfors.data.forEach(operInfo => {
if (operInfo.state === OperationState.progressing) {
operInfo.state = OperationState.interrupt;
operInfo.data.errorInf = 'operation been interrupted';
}
});
this.resultLists = operInfors.data;
}
if (operationInfos.data && operationInfos.data.length) {
// remove expired items
operationInfos.data = operationInfos.data.filter(item => {
return (new Date().getTime() - item.timeStamp) < MAX_SAVING_TIME;
});
operationInfos.data.forEach(operInfo => {
if (operInfo.state === OperationState.progressing) {
operInfo.state = OperationState.interrupt;
operInfo.data.errorInf = 'operation been interrupted';
}
});
this.resultLists = operationInfos.data;
}
}
}
}
}
@ -167,29 +165,31 @@ export class OperationComponent implements OnInit, OnDestroy {
TabEvent(): void {
let timp: any;
let secondsAgo: string, minutesAgo: string, hoursAgo: string, daysAgo: string;
forkJoin([
this.translate.get("OPERATION.SECOND_AGO"),
this.translate.get("OPERATION.MINUTE_AGO"),
this.translate.get("OPERATION.HOUR_AGO"),
this.translate.get("OPERATION.DAY_AGO"),
]).subscribe(res => {
[secondsAgo, minutesAgo, hoursAgo, daysAgo] = res;
});
this.resultLists.forEach(data => {
timp = new Date().getTime() - +data.timeStamp;
data.timeDiff = this.calculateTime(timp);
const timeDiff: number = new Date().getTime() - +data.timeStamp;
data.timeDiff = this.calculateTime(timeDiff, secondsAgo, minutesAgo, hoursAgo, daysAgo);
});
}
calculateTime(timp: number) {
let dist = Math.floor(timp / 1000 / 60); // change to minute;
calculateTime(timeDiff: number, s: string, m: string, h: string, d: string) {
const dist = Math.floor(timeDiff / 1000 / 60); // change to minute;
if (dist > 0 && dist < 60) {
return Math.floor(dist) + ' minute(s) ago';
return Math.floor(dist) + m;
} else if (dist >= 60 && Math.floor(dist / 60) < 24) {
return Math.floor(dist / 60) + ' hour(s) ago';
return Math.floor(dist / 60) + h;
} else if (Math.floor(dist / 60) >= 24) {
return Math.floor(dist / 60 / 24) + ' day(s) ago';
return Math.floor(dist / 60 / 24) + d;
} else {
return 'less than 1 minute';
return s;
}
}
translateTime(tim: string, param?: number) {
this.translate.get(tim, {'param': param}).subscribe((res: string) => {
return res;
});
}
}

View File

@ -87,23 +87,23 @@ export const QuotaUnits = [
UNIT: "Byte",
},
{
UNIT: "KB",
UNIT: "KiB",
},
{
UNIT: "MB",
UNIT: "MiB",
},
{
UNIT: "GB",
UNIT: "GiB",
},
{
UNIT: "TB",
UNIT: "TiB",
},
];
export const QuotaUnlimited = -1;
export const StorageMultipleConstant = 1024;
export const LimitCount = 100000000;
export enum QuotaUnit {
TB = "TB", GB = "GB", MB = "MB", KB = "KB", BIT = "Byte"
TB = "TiB", GB = "GiB", MB = "MiB", KB = "KiB", BIT = "Byte"
}
export enum QuotaProgress {
COUNT_USED = "COUNT_USED", COUNT_HARD = "COUNT_HARD", STROAGE_USED = "STORAGE_USED", STORAGE_HARD = "STORAGE_HARD"

View File

@ -1,5 +1,6 @@
import { delUrlParam, getQueryString, getSizeNumber, getSizeUnit, getSortingString, isSameArrayValue, isSameObject } from "./utils";
import { ClrDatagridStateInterface } from "@clr/angular";
import {QuotaUnit} from "../entities/shared.const";
describe('functions in utils.ts should work', () => {
it('function isSameArrayValue() should work', () => {
@ -60,13 +61,15 @@ describe('functions in utils.ts should work', () => {
expect(getSizeNumber(10)).toEqual(10);
expect(getSizeNumber(456400)).toEqual('445.70');
expect(getSizeNumber(45640000)).toEqual('43.53');
expect(getSizeNumber(4564000000000)).toEqual('4.15');
});
it('function getSizeUnit() should work', () => {
expect(getSizeUnit).toBeTruthy();
expect(getSizeUnit(4564)).toEqual('KB');
expect(getSizeUnit(10)).toEqual('Byte');
expect(getSizeUnit(4564000)).toEqual('MB');
expect(getSizeUnit(4564000000)).toEqual('GB');
expect(getSizeUnit(4564)).toEqual(QuotaUnit.KB);
expect(getSizeUnit(10)).toEqual(QuotaUnit.BIT);
expect(getSizeUnit(4564000)).toEqual(QuotaUnit.MB);
expect(getSizeUnit(4564000000)).toEqual(QuotaUnit.GB);
expect(getSizeUnit(4564000000000)).toEqual(QuotaUnit.TB);
});
});

View File

@ -3,7 +3,7 @@ import { HttpHeaders } from '@angular/common/http';
import { RequestQueryParams } from '../services';
import { DebugElement } from '@angular/core';
import { Comparator, State, HttpOptionInterface, HttpOptionTextInterface, QuotaUnitInterface } from '../services';
import { QuotaUnits, StorageMultipleConstant } from '../entities/shared.const';
import {QuotaUnit, QuotaUnits, StorageMultipleConstant} from '../entities/shared.const';
import { AbstractControl } from "@angular/forms";
import { isValidCron } from 'cron-validator';
import { ClrDatagridStateInterface } from "@clr/angular";
@ -567,13 +567,15 @@ export const validateLimit = unitContrl => {
};
export function formatSize(tagSize: string): string {
let size: number = Number.parseInt(tagSize);
const size: number = Number.parseInt(tagSize);
if (Math.pow(1024, 1) <= size && size < Math.pow(1024, 2)) {
return (size / Math.pow(1024, 1)).toFixed(2) + "KB";
return (size / Math.pow(1024, 1)).toFixed(2) + "KiB";
} else if (Math.pow(1024, 2) <= size && size < Math.pow(1024, 3)) {
return (size / Math.pow(1024, 2)).toFixed(2) + "MB";
return (size / Math.pow(1024, 2)).toFixed(2) + "MiB";
} else if (Math.pow(1024, 3) <= size && size < Math.pow(1024, 4)) {
return (size / Math.pow(1024, 3)).toFixed(2) + "GB";
return (size / Math.pow(1024, 3)).toFixed(2) + "GiB";
} else if (Math.pow(1024, 4) <= size) {
return (size / Math.pow(1024, 4)).toFixed(2) + "TiB";
} else {
return size + "B";
}
@ -590,6 +592,8 @@ export function getSizeNumber(size: number): string | number {
return (size / Math.pow(1024, 2)).toFixed(2);
} else if (Math.pow(1024, 3) <= size && size < Math.pow(1024, 4)) {
return (size / Math.pow(1024, 3)).toFixed(2);
} else if (Math.pow(1024, 4) <= size) {
return (size / Math.pow(1024, 4)).toFixed(2);
} else {
return size;
}
@ -601,13 +605,15 @@ export function getSizeNumber(size: number): string | number {
*/
export function getSizeUnit(size: number): string {
if (Math.pow(1024, 1) <= size && size < Math.pow(1024, 2)) {
return "KB";
return QuotaUnit.KB;
} else if (Math.pow(1024, 2) <= size && size < Math.pow(1024, 3)) {
return "MB";
return QuotaUnit.MB;
} else if (Math.pow(1024, 3) <= size && size < Math.pow(1024, 4)) {
return "GB";
return QuotaUnit.GB;
} else if (Math.pow(1024, 4) <= size) {
return QuotaUnit.TB;
} else {
return "Byte";
return QuotaUnit.BIT;
}
}

View File

@ -14,6 +14,8 @@
"INVALID_MSG": "Falscher Nutzername oder Passwort.",
"FORGOT_PWD": "Passwort vergessen",
"HEADER_LINK": "Einloggen",
"OR": "OR",
"VIA_LOCAL_DB": "LOGIN VIA LOCAL DB",
"CORE_SERVICE_NOT_AVAILABLE": "Core Service ist nicht verfügbar"
},
"SIGN_UP": {

View File

@ -14,7 +14,9 @@
"INVALID_MSG": "Invalid user name or password.",
"FORGOT_PWD": "Forgot password",
"HEADER_LINK": "Sign In",
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available."
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available.",
"OR": "OR",
"VIA_LOCAL_DB": "LOGIN VIA LOCAL DB"
},
"SIGN_UP": {
"TITLE": "Sign Up"

View File

@ -14,7 +14,9 @@
"INVALID_MSG": "Nombre o contraseña no válidos.",
"FORGOT_PWD": "Olvidé mi contraseña",
"HEADER_LINK": "Identificarse",
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available."
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available.",
"OR": "OR",
"VIA_LOCAL_DB": "LOGIN VIA LOCAL DB"
},
"SIGN_UP": {
"TITLE": "Registrarse"

View File

@ -14,7 +14,9 @@
"INVALID_MSG": "Nom d'utilisateur ou mot de passe invalide.",
"FORGOT_PWD": "Mot de passe oublié",
"HEADER_LINK": "S'identifier",
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available."
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available.",
"OR": "OR",
"VIA_LOCAL_DB": "LOGIN VIA LOCAL DB"
},
"SIGN_UP": {
"TITLE": "S'inscrire"

View File

@ -14,7 +14,9 @@
"INVALID_MSG": "Usuário ou senha inválidos",
"FORGOT_PWD": "Esqueci a senha",
"HEADER_LINK": "Logar-se",
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available."
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available.",
"OR": "OR",
"VIA_LOCAL_DB": "LOGIN VIA LOCAL DB"
},
"SIGN_UP": {
"TITLE": "Registrar-se"

View File

@ -14,7 +14,9 @@
"INVALID_MSG": "Geçersiz kullanıcı adı veya şifre.",
"FORGOT_PWD": "Parolamı Unuttum",
"HEADER_LINK": "Oturum aç",
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available."
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available.",
"OR": "OR",
"VIA_LOCAL_DB": "LOGIN VIA LOCAL DB"
},
"SIGN_UP": {
"TITLE": "Kayıt ol"

View File

@ -14,7 +14,9 @@
"INVALID_MSG": "用户名或者密码不正确。",
"FORGOT_PWD": "忘记密码",
"HEADER_LINK": "登录",
"CORE_SERVICE_NOT_AVAILABLE": "核心服务不可用。"
"CORE_SERVICE_NOT_AVAILABLE": "核心服务不可用。",
"OR": "或",
"VIA_LOCAL_DB": "通过本地数据库登录"
},
"SIGN_UP": {
"TITLE": "注册"

View File

@ -14,7 +14,9 @@
"INVALID_MSG": "用戶名或者密碼不正確。",
"FORGOT_PWD": "忘記密碼",
"HEADER_LINK": "登錄",
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available."
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available.",
"OR": "OR",
"VIA_LOCAL_DB": "LOGIN VIA LOCAL DB"
},
"SIGN_UP": {
"TITLE": "註冊"

View File

@ -16,6 +16,7 @@ package artifactinfo
import (
"fmt"
"github.com/docker/distribution/reference"
lib_http "github.com/goharbor/harbor/src/lib/http"
"net/http"
"net/url"
@ -50,7 +51,11 @@ func Middleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
log.Debugf("In artifact info middleware, url: %s", req.URL.String())
m, ok := parse(req.URL)
m, ok, err := parse(req.URL)
if err != nil {
lib_http.SendError(rw, err)
return
}
if !ok {
next.ServeHTTP(rw, req)
return
@ -100,7 +105,7 @@ func projectNameFromRepo(repo string) (string, error) {
return components[0], nil
}
func parse(url *url.URL) (map[string]string, bool) {
func parse(url *url.URL) (map[string]string, bool, error) {
path := url.Path
query := url.Query()
m := make(map[string]string)
@ -119,10 +124,15 @@ func parse(url *url.URL) (map[string]string, bool) {
break
}
}
if digest.DigestRegexp.MatchString(m[lib.ReferenceSubexp]) {
m[lib.DigestSubexp] = m[lib.ReferenceSubexp]
} else if ref, ok := m[lib.ReferenceSubexp]; ok {
m[tag] = ref
// parse reference, for invalid reference format, just give 404.
if m[lib.ReferenceSubexp] != "" {
if digest.DigestRegexp.MatchString(m[lib.ReferenceSubexp]) {
m[lib.DigestSubexp] = m[lib.ReferenceSubexp]
} else if reference.TagRegexp.MatchString(m[lib.ReferenceSubexp]) {
m[tag] = m[lib.ReferenceSubexp]
} else {
return m, match, errors.New("invalid reference format").WithCode(errors.NotFoundCode)
}
}
return m, match
return m, match, nil
}

View File

@ -16,6 +16,7 @@ package artifactinfo
import (
"context"
"github.com/goharbor/harbor/src/lib/errors"
"net/http"
"net/http/httptest"
"net/url"
@ -30,6 +31,7 @@ func TestParseURL(t *testing.T) {
input string
expect map[string]string
match bool
rc string
}{
{
input: "/api/projects",
@ -60,8 +62,12 @@ func TestParseURL(t *testing.T) {
{
input: "/v2/development/golang/manifests/shaxxx:**********************************************************************************************************************************",
expect: map[string]string{},
match: false,
expect: map[string]string{
lib.RepositorySubexp: "development/golang",
lib.ReferenceSubexp: "shaxxx:**********************************************************************************************************************************",
"tag": "shaxxx:**********************************************************************************************************************************",
},
match: true,
},
{
input: "/v2/multi/sector/repository/blobs/sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
@ -99,6 +105,12 @@ func TestParseURL(t *testing.T) {
},
match: true,
},
{
input: "/v2/library/centos/manifest/.Invalid",
expect: map[string]string{},
match: false,
rc: errors.NotFoundCode,
},
}
for _, c := range cases {
@ -106,7 +118,10 @@ func TestParseURL(t *testing.T) {
if err != nil {
panic(err)
}
e, m := parse(url)
e, m, err := parse(url)
if err != nil {
assert.True(t, errors.IsErr(err, c.rc))
}
assert.Equal(t, c.match, m)
assert.Equal(t, c.expect, e)
}

View File

@ -237,6 +237,13 @@ func (api *preheatAPI) CreatePolicy(ctx context.Context, params operation.Create
return api.SendError(ctx, err)
}
project, err := api.projectCtl.GetByName(ctx, params.ProjectName)
if err != nil {
return api.SendError(ctx, err)
}
// override project ID
policy.ProjectID = project.ProjectID
_, err = api.preheatCtl.CreatePolicy(ctx, policy)
if err != nil {
return api.SendError(ctx, err)
@ -658,16 +665,16 @@ func convertTaskToPayload(model *task.Task) (*models.Task, error) {
}
return &models.Task{
CreationTime: model.CreationTime.String(),
EndTime: model.EndTime.String(),
CreationTime: model.CreationTime.Format(time.RFC3339),
EndTime: model.EndTime.Format(time.RFC3339),
ExecutionID: model.ExecutionID,
ExtraAttrs: model.ExtraAttrs,
ID: model.ID,
RunCount: model.RunCount,
StartTime: model.StartTime.String(),
StartTime: model.StartTime.Format(time.RFC3339),
Status: model.Status,
StatusMessage: model.StatusMessage,
UpdateTime: model.UpdateTime.String(),
UpdateTime: model.UpdateTime.Format(time.RFC3339),
}, nil
}

View File

@ -375,16 +375,16 @@ func Test_convertTaskToPayload(t *testing.T) {
EndTime: time.Time{},
},
expect: &models.Task{
CreationTime: "0001-01-01 00:00:00 +0000 UTC",
EndTime: "0001-01-01 00:00:00 +0000 UTC",
CreationTime: "0001-01-01T00:00:00Z",
EndTime: "0001-01-01T00:00:00Z",
ExecutionID: 0,
ExtraAttrs: nil,
ID: 0,
RunCount: 0,
StartTime: "0001-01-01 00:00:00 +0000 UTC",
StartTime: "0001-01-01T00:00:00Z",
Status: "",
StatusMessage: "",
UpdateTime: "0001-01-01 00:00:00 +0000 UTC",
UpdateTime: "0001-01-01T00:00:00Z",
},
},
}

View File

@ -109,6 +109,9 @@ func (u *userGroupAPI) ListUserGroups(ctx context.Context, params operation.List
switch authMode {
case common.LDAPAuth:
query.GroupType = common.LDAPGroupType
if params.LdapGroupDn != nil && len(*params.LdapGroupDn) > 0 {
query.LdapGroupDN = *params.LdapGroupDn
}
case common.HTTPAuth:
query.GroupType = common.HTTPGroupType
}

View File

@ -0,0 +1,83 @@
// Code generated by mockery v2.1.0. DO NOT EDIT.
package dao
import (
context "context"
mock "github.com/stretchr/testify/mock"
models "github.com/goharbor/harbor/src/pkg/joblog/models"
time "time"
)
// DAO is an autogenerated mock type for the DAO type
type DAO struct {
mock.Mock
}
// Create provides a mock function with given fields: ctx, jobLog
func (_m *DAO) Create(ctx context.Context, jobLog *models.JobLog) (int64, error) {
ret := _m.Called(ctx, jobLog)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *models.JobLog) int64); ok {
r0 = rf(ctx, jobLog)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *models.JobLog) error); ok {
r1 = rf(ctx, jobLog)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteBefore provides a mock function with given fields: ctx, t
func (_m *DAO) DeleteBefore(ctx context.Context, t time.Time) (int64, error) {
ret := _m.Called(ctx, t)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, time.Time) int64); ok {
r0 = rf(ctx, t)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, time.Time) error); ok {
r1 = rf(ctx, t)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: ctx, uuid
func (_m *DAO) Get(ctx context.Context, uuid string) (*models.JobLog, error) {
ret := _m.Called(ctx, uuid)
var r0 *models.JobLog
if rf, ok := ret.Get(0).(func(context.Context, string) *models.JobLog); ok {
r0 = rf(ctx, uuid)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.JobLog)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, uuid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,83 @@
// Code generated by mockery v2.1.0. DO NOT EDIT.
package joblog
import (
context "context"
mock "github.com/stretchr/testify/mock"
models "github.com/goharbor/harbor/src/pkg/joblog/models"
time "time"
)
// Manager is an autogenerated mock type for the Manager type
type Manager struct {
mock.Mock
}
// Create provides a mock function with given fields: ctx, jobLog
func (_m *Manager) Create(ctx context.Context, jobLog *models.JobLog) (int64, error) {
ret := _m.Called(ctx, jobLog)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *models.JobLog) int64); ok {
r0 = rf(ctx, jobLog)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *models.JobLog) error); ok {
r1 = rf(ctx, jobLog)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteBefore provides a mock function with given fields: ctx, t
func (_m *Manager) DeleteBefore(ctx context.Context, t time.Time) (int64, error) {
ret := _m.Called(ctx, t)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, time.Time) int64); ok {
r0 = rf(ctx, t)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, time.Time) error); ok {
r1 = rf(ctx, t)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: ctx, uuid
func (_m *Manager) Get(ctx context.Context, uuid string) (*models.JobLog, error) {
ret := _m.Called(ctx, uuid)
var r0 *models.JobLog
if rf, ok := ret.Get(0).(func(context.Context, string) *models.JobLog); ok {
r0 = rf(ctx, uuid)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.JobLog)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, uuid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -51,3 +51,5 @@ package pkg
//go:generate mockery --case snake --dir ../../pkg/replication --name Manager --output ./replication --outpkg manager
//go:generate mockery --case snake --dir ../../pkg/label --name Manager --output ./label --outpkg label
//go:generate mockery --case snake --dir ../../pkg/label/dao --name DAO --output ./label/dao --outpkg dao
//go:generate mockery --case snake --dir ../../pkg/joblog --name Manager --output ./joblog --outpkg joblog
//go:generate mockery --case snake --dir ../../pkg/joblog/dao --name DAO --output ./joblog/dao --outpkg dao

View File

@ -3,11 +3,17 @@ set -e
echo "get the conformance testing code..."
git clone https://github.com/opencontainers/distribution-spec.git
echo "create testing project"
STATUS=$(curl -w '%{http_code}' -H 'Content-Type: application/json' -H 'Accept: application/json' -X POST -u "admin:Harbor12345" -s --insecure "https://$IP/api/v2.0/projects" --data '{"project_name":"conformance","metadata":{"public":"false"},"storage_limit":-1}')
if [ $STATUS -ne 201 ]; then
exit 1
fi
function createPro {
echo "create testing project: $2"
STATUS=$(curl -w '%{http_code}' -H 'Content-Type: application/json' -H 'Accept: application/json' -X POST -u "admin:Harbor12345" -s --insecure "https://$1/api/v2.0/projects" --data "{\"project_name\":\"$2\",\"metadata\":{\"public\":\"false\"},\"storage_limit\":-1}")
if [ $STATUS -ne 201 ]; then
echo "fail to create project: $2, rc: $STATUS"
exit 1
fi
}
createPro $1 conformance
createPro $1 crossmount
echo "run conformance test..."
export OCI_ROOT_URL="https://$1"
@ -20,6 +26,7 @@ export OCI_TEST_PUSH=1
export OCI_TEST_PULL=1
export OCI_TEST_CONTENT_DISCOVERY=1
export OCI_TEST_CONTENT_MANAGEMENT=1
export OCI_CROSSMOUNT_NAMESPACE="crossmount/testrepo"
cd ./distribution-spec/conformance
go test .

View File

@ -3,5 +3,5 @@ set -x
set -e
sudo make package_online GOBUILDTAGS="include_oss include_gcs" VERSIONTAG=dev-travis PKGVERSIONTAG=dev-travis UIVERSIONTAG=dev-travis GOBUILDIMAGE=golang:1.15.12 COMPILETAG=compile_golangimage BUILDBIN=true NOTARYFLAG=true CHARTFLAG=true TRIVYFLAG=true HTTPPROXY= PULL_BASE_FROM_DOCKERHUB=false
sudo make package_offline GOBUILDTAGS="include_oss include_gcs" VERSIONTAG=dev-travis PKGVERSIONTAG=dev-travis UIVERSIONTAG=dev-travis GOBUILDIMAGE=golang:1.15.12 COMPILETAG=compile_golangimage BUILDBIN=true NOTARYFLAG=true CHARTFLAG=true TRIVYFLAG=true HTTPPROXY= PULL_BASE_FROM_DOCKERHUB=false
sudo make package_online GOBUILDTAGS="include_oss include_gcs" VERSIONTAG=dev-travis PKGVERSIONTAG=dev-travis UIVERSIONTAG=dev-travis GOBUILDIMAGE=golang:1.16.5 COMPILETAG=compile_golangimage BUILDBIN=true NOTARYFLAG=true CHARTFLAG=true TRIVYFLAG=true HTTPPROXY= PULL_BASE_FROM_DOCKERHUB=false
sudo make package_offline GOBUILDTAGS="include_oss include_gcs" VERSIONTAG=dev-travis PKGVERSIONTAG=dev-travis UIVERSIONTAG=dev-travis GOBUILDIMAGE=golang:1.16.5 COMPILETAG=compile_golangimage BUILDBIN=true NOTARYFLAG=true CHARTFLAG=true TRIVYFLAG=true HTTPPROXY= PULL_BASE_FROM_DOCKERHUB=false

View File

@ -6,6 +6,7 @@ set -e
export POSTGRESQL_HOST=$1
export REGISTRY_URL=http://$1:5000
export CHROME_BIN=chromium-browser
export GO111MODULE=auto
#export DISPLAY=:99.0
#sh -e /etc/init.d/xvfb start

View File

@ -20,9 +20,10 @@ Resource ../../resources/Util.robot
*** Keywords ***
Sign In Harbor
[Arguments] ${url} ${user} ${pw}
Go To ${url}
[Arguments] ${url} ${user} ${pw} ${is_oidc}=${false}
Go To ${url}
Retry Wait Element ${harbor_span_title}
Run Keyword If ${is_oidc}==${true} Retry Element Click ${login_with_db_btn}
Retry Wait Element ${login_name}
Retry Wait Element ${login_pwd}
Input Text ${login_name} ${user}

View File

@ -19,6 +19,7 @@ Documentation This resource provides any keywords related to the Harbor private
${log_oidc_provider_btn} //*[@id='log_oidc']
${login_with_email_btn} //span[contains(., 'Log in with Email')]
${login_with_ldap_btn} //span[contains(., 'Log in with LDAP')]
${login_with_db_btn} //*[@id='login-db']
${dex_login_btn} //*[@id='login']
${dex_pwd_btn} //*[@id='password']
${submit_login_btn} //*[@id='submit-login']

View File

@ -115,7 +115,7 @@ Test Case - Repo Size
Push Image With Tag ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library alpine 2.6 2.6
Go Into Project library
Go Into Repo alpine
Wait Until Page Contains 1.92MB
Wait Until Page Contains 1.92MiB
Close Browser
Test Case - Staticsinfo
@ -517,7 +517,7 @@ Test Case - Create An New Project With Quotas Set
Init Chrome Driver
${d}= Get Current Date result_format=%m%s
${storage_quota}= Set Variable 600
${storage_quota_unit}= Set Variable GB
${storage_quota_unit}= Set Variable GiB
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Create An New Project And Go Into Project project${d} storage_quota=${storage_quota} storage_quota_unit=${storage_quota_unit}
${storage_quota_ret}= Get Project Storage Quota Text From Project Quotas List project${d}
@ -528,11 +528,11 @@ Test Case - Project Storage Quotas Dispaly And Control
Init Chrome Driver
${d}= Get Current Date result_format=%m%s
${storage_quota}= Set Variable 350
${storage_quota_unit}= Set Variable MB
${storage_quota_unit}= Set Variable MiB
${image_a}= Set Variable one_layer
${image_b}= Set Variable redis
${image_a_size}= Set Variable 330.83MB
${image_b_size}= Set Variable 34.1\\dMB
${image_a_size}= Set Variable 330.83MiB
${image_b_size}= Set Variable 34.1\\dMiB
${image_a_ver}= Set Variable 1.0
${image_b_ver}= Set Variable donotremove5.0
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
@ -561,7 +561,7 @@ Test Case - Project Quotas Control Under Copy
${image_a_ver}= Set Variable donotremove5.0
${image_b_ver}= Set Variable do_not_remove_6.8.3
${storage_quota}= Set Variable 330
${storage_quota_unit}= Set Variable MB
${storage_quota_unit}= Set Variable MiB
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Create An New Project And Go Into Project project_a_${d}
Create An New Project And Go Into Project project_b_${d} storage_quota=${storage_quota} storage_quota_unit=${storage_quota_unit}

View File

@ -74,9 +74,9 @@ Test Case - Project Quotas Control Under GC
Init Chrome Driver
${d}= Get Current Date result_format=%m%s
${storage_quota}= Set Variable 200
${storage_quota_unit}= Set Variable MB
${storage_quota_unit}= Set Variable MiB
${image_a}= Set Variable logstash
${image_a_size}= Set Variable 321.03MB
${image_a_size}= Set Variable 321.03MiB
${image_a_ver}= Set Variable 6.8.3
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Create An New Project And Go Into Project project${d} storage_quota=${storage_quota} storage_quota_unit=${storage_quota_unit}

View File

@ -90,17 +90,17 @@ Test Case - Helm CLI Push
Test Case - Onboard OIDC User Sign In
Init Chrome Driver
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} is_oidc=${true}
Check Automatic Onboarding And Save
Logout Harbor
Sign In Harbor With OIDC User ${HARBOR_URL} test8 is_onboard=${true}
Logout Harbor
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} is_oidc=${true}
Set User Name Claim And Save email
Logout Harbor
Sign In Harbor With OIDC User ${HARBOR_URL} test9 is_onboard=${true} username_claim=email
Logout Harbor
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} is_oidc=${true}
Set User Name Claim And Save ${null}
Sleep 2
Close Browser

View File

@ -5,6 +5,7 @@ ARG MOCKERY_VERSION
# https://github.com/docker-library/golang/issues/225
ENV XDG_CACHE_HOME /tmp
ENV GO111MODULE auto
RUN mkdir -p /tmp/mockery-${MOCKERY_VERSION} && \
curl -fsSL https://github.com/vektra/mockery/releases/download/${MOCKERY_VERSION}/mockery_${MOCKERY_VERSION#v}_Linux_x86_64.tar.gz | tar -xz -C /tmp/mockery-${MOCKERY_VERSION} && \