mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-26 12:15:20 +01:00
Enhance the read-only API to avoid deleting operations during the job running (#17055)
Enhance the read-only API to avoid deleting operations during the job running Fixes #16901 Signed-off-by: Wenkai Yin(尹文开) <yinw@vmware.com>
This commit is contained in:
parent
77d28105bc
commit
ab74e853ee
@ -19,6 +19,7 @@ import (
|
|||||||
|
|
||||||
"github.com/goharbor/harbor/src/common"
|
"github.com/goharbor/harbor/src/common"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/registry/interceptor/readonly"
|
||||||
"github.com/goharbor/harbor/src/registryctl/client"
|
"github.com/goharbor/harbor/src/registryctl/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -42,5 +43,5 @@ func initRegistryCtlClient() {
|
|||||||
cfg := &client.Config{
|
cfg := &client.Config{
|
||||||
Secret: os.Getenv("JOBSERVICE_SECRET"),
|
Secret: os.Getenv("JOBSERVICE_SECRET"),
|
||||||
}
|
}
|
||||||
RegistryCtlClient = client.NewClient(registryCtlURL, cfg)
|
RegistryCtlClient = client.NewClient(registryCtlURL, cfg, readonly.NewInterceptor())
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/pkg/artifactrash/model"
|
"github.com/goharbor/harbor/src/pkg/artifactrash/model"
|
||||||
"github.com/goharbor/harbor/src/pkg/blob"
|
"github.com/goharbor/harbor/src/pkg/blob"
|
||||||
blobModels "github.com/goharbor/harbor/src/pkg/blob/models"
|
blobModels "github.com/goharbor/harbor/src/pkg/blob/models"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/registry/interceptor/readonly"
|
||||||
"github.com/goharbor/harbor/src/registryctl/client"
|
"github.com/goharbor/harbor/src/registryctl/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -289,6 +290,10 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
|
|||||||
gc.logger.Errorf("[%d/%d] failed to call gc.markDeleteFailed() after v2DeleteManifest() error out: %s, %v", idx, total, blob.Digest, err)
|
gc.logger.Errorf("[%d/%d] failed to call gc.markDeleteFailed() after v2DeleteManifest() error out: %s, %v", idx, total, blob.Digest, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// if the system is set to read-only mode, return directly
|
||||||
|
if err == readonly.Err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
skippedBlob = true
|
skippedBlob = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -296,7 +301,12 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
|
|||||||
gc.logger.Infof("[%d/%d] delete manifest from storage: %s", idx, total, blob.Digest)
|
gc.logger.Infof("[%d/%d] delete manifest from storage: %s", idx, total, blob.Digest)
|
||||||
if err := retry.Retry(func() error {
|
if err := retry.Retry(func() error {
|
||||||
return ignoreNotFound(func() error {
|
return ignoreNotFound(func() error {
|
||||||
return gc.registryCtlClient.DeleteManifest(art.RepositoryName, blob.Digest)
|
err := gc.registryCtlClient.DeleteManifest(art.RepositoryName, blob.Digest)
|
||||||
|
// if the system is in read-only mode, return an Abort error to skip retrying
|
||||||
|
if err == readonly.Err {
|
||||||
|
return retry.Abort(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
}, retry.Callback(func(err error, sleep time.Duration) {
|
}, retry.Callback(func(err error, sleep time.Duration) {
|
||||||
gc.logger.Infof("[%d/%d] failed to exec DeleteManifest, error: %v, will retry again after: %s", idx, total, err, sleep)
|
gc.logger.Infof("[%d/%d] failed to exec DeleteManifest, error: %v, will retry again after: %s", idx, total, err, sleep)
|
||||||
@ -308,6 +318,10 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
|
|||||||
gc.logger.Errorf("[%d/%d] failed to call gc.markDeleteFailed() after gc.registryCtlClient.DeleteManifest() error out: %s, %s, %v", idx, total, art.RepositoryName, blob.Digest, err)
|
gc.logger.Errorf("[%d/%d] failed to call gc.markDeleteFailed() after gc.registryCtlClient.DeleteManifest() error out: %s, %s, %v", idx, total, art.RepositoryName, blob.Digest, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// if the system is set to read-only mode, return directly
|
||||||
|
if err == readonly.Err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
skippedBlob = true
|
skippedBlob = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -333,7 +347,12 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
|
|||||||
gc.logger.Infof("[%d/%d] delete blob from storage: %s", idx, total, blob.Digest)
|
gc.logger.Infof("[%d/%d] delete blob from storage: %s", idx, total, blob.Digest)
|
||||||
if err := retry.Retry(func() error {
|
if err := retry.Retry(func() error {
|
||||||
return ignoreNotFound(func() error {
|
return ignoreNotFound(func() error {
|
||||||
return gc.registryCtlClient.DeleteBlob(blob.Digest)
|
err := gc.registryCtlClient.DeleteBlob(blob.Digest)
|
||||||
|
// if the system is in read-only mode, return an Abort error to skip retrying
|
||||||
|
if err == readonly.Err {
|
||||||
|
return retry.Abort(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
}, retry.Callback(func(err error, sleep time.Duration) {
|
}, retry.Callback(func(err error, sleep time.Duration) {
|
||||||
gc.logger.Infof("[%d/%d] failed to exec DeleteBlob, error: %v, will retry again after: %s", idx, total, err, sleep)
|
gc.logger.Infof("[%d/%d] failed to exec DeleteBlob, error: %v, will retry again after: %s", idx, total, err, sleep)
|
||||||
@ -345,6 +364,10 @@ func (gc *GarbageCollector) sweep(ctx job.Context) error {
|
|||||||
gc.logger.Errorf("[%d/%d] failed to call gc.markDeleteFailed() after gc.registryCtlClient.DeleteBlob() error out: %s, %v", idx, total, blob.Digest, err)
|
gc.logger.Errorf("[%d/%d] failed to call gc.markDeleteFailed() after gc.registryCtlClient.DeleteBlob() error out: %s, %v", idx, total, blob.Digest, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// if the system is set to read-only mode, return directly
|
||||||
|
if err == readonly.Err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sweepSize = sweepSize + blob.Size
|
sweepSize = sweepSize + blob.Size
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
"github.com/goharbor/harbor/src/lib/retry"
|
"github.com/goharbor/harbor/src/lib/retry"
|
||||||
"github.com/goharbor/harbor/src/pkg/registry"
|
"github.com/goharbor/harbor/src/pkg/registry"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/registry/interceptor/readonly"
|
||||||
"github.com/gomodule/redigo/redis"
|
"github.com/gomodule/redigo/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -55,7 +56,12 @@ func v2DeleteManifest(logger logger.Interface, repository, digest string) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return retry.Retry(func() error {
|
return retry.Retry(func() error {
|
||||||
return registry.Cli.DeleteManifest(repository, digest)
|
err := registry.Cli.DeleteManifest(repository, digest)
|
||||||
|
// if the system is in read-only mode, return an Abort error to skip retrying
|
||||||
|
if err == readonly.Err {
|
||||||
|
return retry.Abort(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}, retry.Callback(func(err error, sleep time.Duration) {
|
}, retry.Callback(func(err error, sleep time.Duration) {
|
||||||
logger.Infof("failed to exec v2DeleteManifest, error: %v, will retry again after: %s", err, sleep)
|
logger.Infof("failed to exec v2DeleteManifest, error: %v, will retry again after: %s", err, sleep)
|
||||||
}))
|
}))
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/common"
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
"github.com/goharbor/harbor/src/jobservice/logger/backend"
|
"github.com/goharbor/harbor/src/jobservice/logger/backend"
|
||||||
|
"github.com/goharbor/harbor/src/lib/config"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@ -11,6 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
config.DefaultCfgManager = common.InMemoryCfgManager
|
||||||
|
|
||||||
// databases := []string{"mysql", "sqlite"}
|
// databases := []string{"mysql", "sqlite"}
|
||||||
databases := []string{"postgresql"}
|
databases := []string{"postgresql"}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/jobservice/logger/sweeper"
|
"github.com/goharbor/harbor/src/jobservice/logger/sweeper"
|
||||||
|
"github.com/goharbor/harbor/src/lib/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -88,6 +89,15 @@ func (c *SweeperController) startSweeper(s sweeper.Interface) {
|
|||||||
func (c *SweeperController) doSweeping(sid string, s sweeper.Interface) {
|
func (c *SweeperController) doSweeping(sid string, s sweeper.Interface) {
|
||||||
Debugf("Sweeper %s is under working", sid)
|
Debugf("Sweeper %s is under working", sid)
|
||||||
|
|
||||||
|
if err := config.Load(context.Background()); err != nil {
|
||||||
|
c.errChan <- fmt.Errorf("failed to load configurations: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.ReadOnly(context.Background()) {
|
||||||
|
c.errChan <- fmt.Errorf("the system is in read only mode, cancel the sweeping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
count, err := s.Sweep()
|
count, err := s.Sweep()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.errChan <- fmt.Errorf("sweep logs error in %s at %d: %s", sid, time.Now().Unix(), err)
|
c.errChan <- fmt.Errorf("sweep logs error in %s at %d: %s", sid, time.Now().Unix(), err)
|
||||||
|
@ -20,11 +20,14 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common"
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
"github.com/goharbor/harbor/src/jobservice/common/utils"
|
"github.com/goharbor/harbor/src/jobservice/common/utils"
|
||||||
"github.com/goharbor/harbor/src/jobservice/config"
|
"github.com/goharbor/harbor/src/jobservice/config"
|
||||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||||
"github.com/goharbor/harbor/src/jobservice/tests"
|
"github.com/goharbor/harbor/src/jobservice/tests"
|
||||||
|
libcfg "github.com/goharbor/harbor/src/lib/config"
|
||||||
|
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
@ -42,6 +45,8 @@ type BootStrapTestSuite struct {
|
|||||||
func (suite *BootStrapTestSuite) SetupSuite() {
|
func (suite *BootStrapTestSuite) SetupSuite() {
|
||||||
dao.PrepareTestForPostgresSQL()
|
dao.PrepareTestForPostgresSQL()
|
||||||
|
|
||||||
|
libcfg.DefaultCfgManager = common.InMemoryCfgManager
|
||||||
|
|
||||||
// Load configurations
|
// Load configurations
|
||||||
err := config.DefaultConfig.Load("../config_test.yml", true)
|
err := config.DefaultConfig.Load("../config_test.yml", true)
|
||||||
require.NoError(suite.T(), err, "load configurations error: %s", err)
|
require.NoError(suite.T(), err, "load configurations error: %s", err)
|
||||||
|
4
src/lib/cache/memory/memory.go
vendored
4
src/lib/cache/memory/memory.go
vendored
@ -75,7 +75,9 @@ func (c *Cache) Fetch(ctx context.Context, key string, value interface{}) error
|
|||||||
e := v.(*entry)
|
e := v.(*entry)
|
||||||
if e.isExpirated() {
|
if e.isExpirated() {
|
||||||
err := c.Delete(ctx, c.opts.Key(key))
|
err := c.Delete(ctx, c.opts.Key(key))
|
||||||
log.Errorf("failed to delete cache in Fetch() method when it's expired, error: %v", err)
|
if err != nil {
|
||||||
|
log.Errorf("failed to delete cache in Fetch() method when it's expired, error: %v", err)
|
||||||
|
}
|
||||||
return cache.ErrNotFound
|
return cache.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,8 +26,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/lib/config"
|
|
||||||
|
|
||||||
"github.com/docker/distribution"
|
"github.com/docker/distribution"
|
||||||
"github.com/docker/distribution/manifest/manifestlist"
|
"github.com/docker/distribution/manifest/manifestlist"
|
||||||
_ "github.com/docker/distribution/manifest/ocischema" // register oci manifest unmarshal function
|
_ "github.com/docker/distribution/manifest/ocischema" // register oci manifest unmarshal function
|
||||||
@ -35,8 +33,11 @@ import (
|
|||||||
"github.com/docker/distribution/manifest/schema2"
|
"github.com/docker/distribution/manifest/schema2"
|
||||||
commonhttp "github.com/goharbor/harbor/src/common/http"
|
commonhttp "github.com/goharbor/harbor/src/common/http"
|
||||||
"github.com/goharbor/harbor/src/lib"
|
"github.com/goharbor/harbor/src/lib"
|
||||||
|
"github.com/goharbor/harbor/src/lib/config"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
"github.com/goharbor/harbor/src/pkg/registry/auth"
|
"github.com/goharbor/harbor/src/pkg/registry/auth"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/registry/interceptor"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/registry/interceptor/readonly"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
)
|
)
|
||||||
@ -46,7 +47,7 @@ var (
|
|||||||
Cli = func() Client {
|
Cli = func() Client {
|
||||||
url, _ := config.RegistryURL()
|
url, _ := config.RegistryURL()
|
||||||
username, password := config.RegistryCredential()
|
username, password := config.RegistryCredential()
|
||||||
return NewClient(url, username, password, false)
|
return NewClient(url, username, password, false, readonly.NewInterceptor())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
accepts = []string{
|
accepts = []string{
|
||||||
@ -101,10 +102,17 @@ type Client interface {
|
|||||||
// NewClient creates a registry client with the default authorizer which determines the auth scheme
|
// NewClient creates a registry client with the default authorizer which determines the auth scheme
|
||||||
// of the registry automatically and calls the corresponding underlying authorizers(basic/bearer) to
|
// of the registry automatically and calls the corresponding underlying authorizers(basic/bearer) to
|
||||||
// do the auth work. If a customized authorizer is needed, use "NewClientWithAuthorizer" instead
|
// do the auth work. If a customized authorizer is needed, use "NewClientWithAuthorizer" instead
|
||||||
func NewClient(url, username, password string, insecure bool) Client {
|
func NewClient(url, username, password string, insecure bool, interceptors ...interceptor.Interceptor) Client {
|
||||||
|
authorizer := auth.NewAuthorizer(username, password, insecure)
|
||||||
|
return NewClientWithAuthorizer(url, authorizer, insecure, interceptors...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClientWithAuthorizer creates a registry client with the provided authorizer
|
||||||
|
func NewClientWithAuthorizer(url string, authorizer lib.Authorizer, insecure bool, interceptors ...interceptor.Interceptor) Client {
|
||||||
return &client{
|
return &client{
|
||||||
url: url,
|
url: url,
|
||||||
authorizer: auth.NewAuthorizer(username, password, insecure),
|
authorizer: authorizer,
|
||||||
|
interceptors: interceptors,
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Transport: commonhttp.GetHTTPTransport(commonhttp.WithInsecure(insecure)),
|
Transport: commonhttp.GetHTTPTransport(commonhttp.WithInsecure(insecure)),
|
||||||
Timeout: 30 * time.Minute,
|
Timeout: 30 * time.Minute,
|
||||||
@ -112,21 +120,11 @@ func NewClient(url, username, password string, insecure bool) Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClientWithAuthorizer creates a registry client with the provided authorizer
|
|
||||||
func NewClientWithAuthorizer(url string, authorizer lib.Authorizer, insecure bool) Client {
|
|
||||||
return &client{
|
|
||||||
url: url,
|
|
||||||
authorizer: authorizer,
|
|
||||||
client: &http.Client{
|
|
||||||
Transport: commonhttp.GetHTTPTransport(commonhttp.WithInsecure(insecure)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
url string
|
url string
|
||||||
authorizer lib.Authorizer
|
authorizer lib.Authorizer
|
||||||
client *http.Client
|
interceptors []interceptor.Interceptor
|
||||||
|
client *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) Ping() error {
|
func (c *client) Ping() error {
|
||||||
@ -510,6 +508,11 @@ func (c *client) Do(req *http.Request) (*http.Response, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) do(req *http.Request) (*http.Response, error) {
|
func (c *client) do(req *http.Request) (*http.Response, error) {
|
||||||
|
for _, interceptor := range c.interceptors {
|
||||||
|
if err := interceptor.Intercept(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if c.authorizer != nil {
|
if c.authorizer != nil {
|
||||||
if err := c.authorizer.Modify(req); err != nil {
|
if err := c.authorizer.Modify(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
22
src/pkg/registry/interceptor/interceptor.go
Normal file
22
src/pkg/registry/interceptor/interceptor.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// 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 interceptor
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// Interceptor intercepts the request
|
||||||
|
type Interceptor interface {
|
||||||
|
Intercept(req *http.Request) error
|
||||||
|
}
|
88
src/pkg/registry/interceptor/readonly/interceptor.go
Normal file
88
src/pkg/registry/interceptor/readonly/interceptor.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// 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 readonly
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/goharbor/harbor/src/lib/cache/memory"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/lib/cache"
|
||||||
|
"github.com/goharbor/harbor/src/lib/config"
|
||||||
|
itcp "github.com/goharbor/harbor/src/pkg/registry/interceptor"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Err indicates the system is in read only mode
|
||||||
|
var (
|
||||||
|
Err = errors.New("the system is in read only mode, cancel the request")
|
||||||
|
key = "read-only"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewInterceptor creates an interceptor that intercepts any requests if the system is set to read-only
|
||||||
|
func NewInterceptor() itcp.Interceptor {
|
||||||
|
// ignore the error as the New return nil error
|
||||||
|
cache, _ := memory.New(cache.Options{
|
||||||
|
Expiration: 5 * time.Second,
|
||||||
|
Codec: cache.DefaultCodec(),
|
||||||
|
})
|
||||||
|
return &interceptor{cache: cache}
|
||||||
|
}
|
||||||
|
|
||||||
|
type interceptor struct {
|
||||||
|
cache cache.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *interceptor) Intercept(req *http.Request) error {
|
||||||
|
switch req.Method {
|
||||||
|
case http.MethodGet, http.MethodHead, http.MethodOptions:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
isReadOnly, err := i.isReadOnly(req.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isReadOnly {
|
||||||
|
return Err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *interceptor) isReadOnly(ctx context.Context) (bool, error) {
|
||||||
|
var (
|
||||||
|
isReadOnly bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
// return the cached value if exists
|
||||||
|
if err = i.cache.Fetch(ctx, key, &isReadOnly); err == nil {
|
||||||
|
return isReadOnly, nil
|
||||||
|
}
|
||||||
|
if err != cache.ErrNotFound {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// no cache, get the config via API
|
||||||
|
if err := config.Load(ctx); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
isReadOnly = config.ReadOnly(ctx)
|
||||||
|
if err := i.cache.Save(ctx, key, &isReadOnly); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return isReadOnly, nil
|
||||||
|
}
|
59
src/pkg/registry/interceptor/readonly/interceptor_test.go
Normal file
59
src/pkg/registry/interceptor/readonly/interceptor_test.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// 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 readonly
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common"
|
||||||
|
"github.com/goharbor/harbor/src/lib/cache"
|
||||||
|
"github.com/goharbor/harbor/src/lib/cache/memory"
|
||||||
|
"github.com/goharbor/harbor/src/lib/config"
|
||||||
|
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIntercept(t *testing.T) {
|
||||||
|
cache, _ := memory.New(cache.Options{
|
||||||
|
Expiration: 1 * time.Nanosecond,
|
||||||
|
Codec: cache.DefaultCodec(),
|
||||||
|
})
|
||||||
|
interceptor := &interceptor{
|
||||||
|
cache: cache,
|
||||||
|
}
|
||||||
|
|
||||||
|
// method: GET
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, "", nil)
|
||||||
|
assert.Nil(t, interceptor.Intercept(req))
|
||||||
|
|
||||||
|
config.DefaultCfgManager = common.InMemoryCfgManager
|
||||||
|
|
||||||
|
// method: DELETE
|
||||||
|
// read only enable: false
|
||||||
|
req, _ = http.NewRequest(http.MethodDelete, "", nil)
|
||||||
|
assert.Nil(t, interceptor.Intercept(req))
|
||||||
|
|
||||||
|
// method: DELETE
|
||||||
|
// read only enable: true
|
||||||
|
req, _ = http.NewRequest(http.MethodDelete, "", nil)
|
||||||
|
err := config.DefaultMgr().UpdateConfig(context.Background(), map[string]interface{}{common.ReadOnly: true})
|
||||||
|
require.Nil(t, err)
|
||||||
|
time.Sleep(1 * time.Nanosecond) // make sure the cached key is expired
|
||||||
|
assert.Equal(t, Err, interceptor.Intercept(req))
|
||||||
|
}
|
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/http/modifier/auth"
|
"github.com/goharbor/harbor/src/common/http/modifier/auth"
|
||||||
"github.com/goharbor/harbor/src/common/utils"
|
"github.com/goharbor/harbor/src/common/utils"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/registry/interceptor"
|
||||||
)
|
)
|
||||||
|
|
||||||
// const definition
|
// const definition
|
||||||
@ -42,8 +43,9 @@ type Client interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
client *common_http.Client
|
client *common_http.Client
|
||||||
|
interceptors []interceptor.Interceptor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config contains configurations needed for client
|
// Config contains configurations needed for client
|
||||||
@ -52,13 +54,14 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewClient return an instance of Registry client
|
// NewClient return an instance of Registry client
|
||||||
func NewClient(baseURL string, cfg *Config) Client {
|
func NewClient(baseURL string, cfg *Config, interceptors ...interceptor.Interceptor) Client {
|
||||||
baseURL = strings.TrimRight(baseURL, "/")
|
baseURL = strings.TrimRight(baseURL, "/")
|
||||||
if !strings.Contains(baseURL, "://") {
|
if !strings.Contains(baseURL, "://") {
|
||||||
baseURL = "http://" + baseURL
|
baseURL = "http://" + baseURL
|
||||||
}
|
}
|
||||||
client := &client{
|
client := &client{
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
|
interceptors: interceptors,
|
||||||
}
|
}
|
||||||
if cfg != nil {
|
if cfg != nil {
|
||||||
authorizer := auth.NewSecretAuthorizer(cfg.Secret)
|
authorizer := auth.NewSecretAuthorizer(cfg.Secret)
|
||||||
@ -105,6 +108,11 @@ func (c *client) DeleteManifest(repository, reference string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) do(req *http.Request) (*http.Response, error) {
|
func (c *client) do(req *http.Request) (*http.Response, error) {
|
||||||
|
for _, interceptor := range c.interceptors {
|
||||||
|
if err := interceptor.Intercept(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
req.Header.Set("User-Agent", UserAgent)
|
req.Header.Set("User-Agent", UserAgent)
|
||||||
resp, err := c.client.Do(req)
|
resp, err := c.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user