mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-21 08:07:59 +01:00
feat(preheat):add preheat drivers
- define preheat driver interface - implement dragonfly driver - implememt kraken driver - add related UT cases with testify framework - fix #10870 #10871 - some code are picked up from the original P2P feat branch Signed-off-by: Steven Zou <szou@vmware.com>
This commit is contained in:
parent
ce62d05321
commit
df86ae1ad0
48
src/pkg/p2p/preheat/models/provider/instance.go
Normal file
48
src/pkg/p2p/preheat/models/provider/instance.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// 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 provider
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PreheatingImageTypeImage defines the 'image' type of preheating images
|
||||||
|
PreheatingImageTypeImage = "image"
|
||||||
|
// PreheatingStatusPending means the preheating is waiting for starting
|
||||||
|
PreheatingStatusPending = "PENDING"
|
||||||
|
// PreheatingStatusRunning means the preheating is ongoing
|
||||||
|
PreheatingStatusRunning = "RUNNING"
|
||||||
|
// PreheatingStatusSuccess means the preheating is success
|
||||||
|
PreheatingStatusSuccess = "SUCCESS"
|
||||||
|
// PreheatingStatusFail means the preheating is failed
|
||||||
|
PreheatingStatusFail = "FAIL"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Instance defines the properties of the preheating provider instance.
|
||||||
|
type Instance struct {
|
||||||
|
ID int64 `orm:"pk;auto;column(id)" json:"id"`
|
||||||
|
Name string `orm:"column(name)" json:"name"`
|
||||||
|
Description string `orm:"column(description)" json:"description"`
|
||||||
|
Vendor string `orm:"column(vendor)" json:"vendor"`
|
||||||
|
Endpoint string `orm:"column(endpoint)" json:"endpoint"`
|
||||||
|
AuthMode string `orm:"column(auth_mode)" json:"auth_mode"`
|
||||||
|
// The auth credential data if exists
|
||||||
|
AuthInfo map[string]string `orm:"column(-)" json:"auth_info,omitempty"`
|
||||||
|
// Data format for "AuthInfo"
|
||||||
|
AuthData string `orm:"column(auth_data)" json:"-"`
|
||||||
|
// Default 'Unknown', use separate API for client to retrieve
|
||||||
|
Status string `orm:"column(-)" json:"status"`
|
||||||
|
Enabled bool `orm:"column(enabled)" json:"enabled"`
|
||||||
|
Default bool `orm:"column(is_default)" json:"default"`
|
||||||
|
Insecure bool `orm:"column(insecure)" json:"insecure"`
|
||||||
|
SetupTimestamp int64 `orm:"column(setup_timestamp)" json:"setup_timestamp"`
|
||||||
|
}
|
33
src/pkg/p2p/preheat/provider/auth/basic_handler.go
Normal file
33
src/pkg/p2p/preheat/provider/auth/basic_handler.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BasicAuthHandler handle the basic auth mode.
|
||||||
|
type BasicAuthHandler struct {
|
||||||
|
*BaseHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode implements @Handler.Mode
|
||||||
|
func (b *BasicAuthHandler) Mode() string {
|
||||||
|
return AuthModeBasic
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorize implements @Handler.Authorize
|
||||||
|
func (b *BasicAuthHandler) Authorize(req *http.Request, cred *Credential) error {
|
||||||
|
if err := b.BaseHandler.Authorize(req, cred); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cred.Data) == 0 {
|
||||||
|
return errors.New("missing username and/or password")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := reflect.ValueOf(cred.Data).MapKeys()[0].String()
|
||||||
|
req.SetBasicAuth(key, cred.Data[key])
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
23
src/pkg/p2p/preheat/provider/auth/cred.go
Normal file
23
src/pkg/p2p/preheat/provider/auth/cred.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AuthModeNone means no auth required
|
||||||
|
AuthModeNone = "NONE"
|
||||||
|
// AuthModeBasic is basic mode
|
||||||
|
AuthModeBasic = "BASIC"
|
||||||
|
// AuthModeOAuth is OAuth mode
|
||||||
|
AuthModeOAuth = "OAUTH"
|
||||||
|
// AuthModeCustom is custom mode
|
||||||
|
AuthModeCustom = "CUSTOM"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Credential stores the related data for authorization.
|
||||||
|
type Credential struct {
|
||||||
|
Mode string
|
||||||
|
|
||||||
|
// Keep the auth data.
|
||||||
|
// If authMode is 'BASIC', then 'username' and 'password' are stored;
|
||||||
|
// If authMode is 'OAUTH', then 'token' is stored'
|
||||||
|
// If authMode is 'CUSTOM', then 'header_key' with corresponding header value are stored.
|
||||||
|
Data map[string]string
|
||||||
|
}
|
33
src/pkg/p2p/preheat/provider/auth/custom_handler.go
Normal file
33
src/pkg/p2p/preheat/provider/auth/custom_handler.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CustomAuthHandler handle the custom auth mode.
|
||||||
|
type CustomAuthHandler struct {
|
||||||
|
*BaseHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode implements @Handler.Mode
|
||||||
|
func (c *CustomAuthHandler) Mode() string {
|
||||||
|
return AuthModeCustom
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorize implements @Handler.Authorize
|
||||||
|
func (c *CustomAuthHandler) Authorize(req *http.Request, cred *Credential) error {
|
||||||
|
if err := c.BaseHandler.Authorize(req, cred); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cred.Data) == 0 {
|
||||||
|
return errors.New("missing custom token/key data")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := reflect.ValueOf(cred.Data).MapKeys()[0].String()
|
||||||
|
req.Header.Set(key, cred.Data[key])
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
40
src/pkg/p2p/preheat/provider/auth/handler.go
Normal file
40
src/pkg/p2p/preheat/provider/auth/handler.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler defines how to add authorization data to the requests
|
||||||
|
// depending on the different auth modes.
|
||||||
|
type Handler interface {
|
||||||
|
// Append authorization data to the request depends on cred modes.
|
||||||
|
//
|
||||||
|
// If everything is ok, nil error will be returned.
|
||||||
|
// Otherwise, an error will be got.
|
||||||
|
Authorize(req *http.Request, cred *Credential) error
|
||||||
|
|
||||||
|
// Mode returns the auth mode identity.
|
||||||
|
Mode() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseHandler provides some basic functions like validation.
|
||||||
|
type BaseHandler struct{}
|
||||||
|
|
||||||
|
// Mode implements @Handler.Mode
|
||||||
|
func (b *BaseHandler) Mode() string {
|
||||||
|
return "BASE"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorize implements @Handler.Authorize
|
||||||
|
func (b *BaseHandler) Authorize(req *http.Request, cred *Credential) error {
|
||||||
|
if req == nil {
|
||||||
|
return errors.New("nil request cannot be authorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cred == nil || cred.Data == nil {
|
||||||
|
return errors.New("no credential data provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
102
src/pkg/p2p/preheat/provider/auth/handler_test.go
Normal file
102
src/pkg/p2p/preheat/provider/auth/handler_test.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// 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 auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
authorizationHeader = "Authorization"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthHandlerTestSuite is test suite for testing auth handler
|
||||||
|
type AuthHandlerTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAuthHandler is the entry method of running AuthHandlerTestSuite
|
||||||
|
func TestAuthHandler(t *testing.T) {
|
||||||
|
suite.Run(t, &AuthHandlerTestSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNoneHandler test none handler
|
||||||
|
func (suite *AuthHandlerTestSuite) TestNoneHandler() {
|
||||||
|
none := &NoneAuthHandler{}
|
||||||
|
suite.Equal(AuthModeNone, none.Mode(), "auth mode None")
|
||||||
|
r, err := http.NewRequest(http.MethodGet, "https://p2p.none.com", nil)
|
||||||
|
require.NoError(suite.T(), err, "new HTTP request")
|
||||||
|
err = none.Authorize(r, nil)
|
||||||
|
require.NoError(suite.T(), err, "authorize HTTP request")
|
||||||
|
suite.Equal(0, len(r.Header.Get(authorizationHeader)), "check authorization header")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBasicHandler test basic auth handler
|
||||||
|
func (suite *AuthHandlerTestSuite) TestBasicHandler() {
|
||||||
|
basic := &BasicAuthHandler{}
|
||||||
|
suite.Equal(AuthModeBasic, basic.Mode(), "auth mode basic")
|
||||||
|
r, err := http.NewRequest(http.MethodGet, "https://p2p.basic.com", nil)
|
||||||
|
require.NoError(suite.T(), err, "new HTTP request")
|
||||||
|
cred := &Credential{
|
||||||
|
Mode: AuthModeBasic,
|
||||||
|
Data: map[string]string{
|
||||||
|
"username": "password",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = basic.Authorize(r, cred)
|
||||||
|
require.NoError(suite.T(), err, "authorize HTTP request")
|
||||||
|
encodedStr := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", "username", "password")))
|
||||||
|
suite.Equal(fmt.Sprintf("%s %s", "Basic", encodedStr), r.Header.Get(authorizationHeader), "check basic authorization header")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTokenHandler test token auth handler
|
||||||
|
func (suite *AuthHandlerTestSuite) TestTokenHandler() {
|
||||||
|
token := &TokenAuthHandler{}
|
||||||
|
suite.Equal(AuthModeOAuth, token.Mode(), "auth mode token")
|
||||||
|
r, err := http.NewRequest(http.MethodGet, "https://p2p.token.com", nil)
|
||||||
|
require.NoError(suite.T(), err, "new HTTP request")
|
||||||
|
cred := &Credential{
|
||||||
|
Mode: AuthModeOAuth,
|
||||||
|
Data: map[string]string{
|
||||||
|
"token": "my-token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = token.Authorize(r, cred)
|
||||||
|
require.NoError(suite.T(), err, "authorize HTTP request")
|
||||||
|
suite.Equal("Bearer my-token", r.Header.Get(authorizationHeader), "check token authorization header")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCustomHandler test custom auth handler
|
||||||
|
func (suite *AuthHandlerTestSuite) TestCustomHandler() {
|
||||||
|
custom := &CustomAuthHandler{}
|
||||||
|
suite.Equal(AuthModeCustom, custom.Mode(), "auth mode custom")
|
||||||
|
r, err := http.NewRequest(http.MethodGet, "https://p2p.custom.com", nil)
|
||||||
|
require.NoError(suite.T(), err, "new HTTP request")
|
||||||
|
cred := &Credential{
|
||||||
|
Mode: AuthModeCustom,
|
||||||
|
Data: map[string]string{
|
||||||
|
"api-key": "my-api-key",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = custom.Authorize(r, cred)
|
||||||
|
require.NoError(suite.T(), err, "authorize HTTP request")
|
||||||
|
suite.Equal("my-api-key", r.Header.Get("api-key"), "check custom authorization header")
|
||||||
|
}
|
16
src/pkg/p2p/preheat/provider/auth/known_handlers.go
Normal file
16
src/pkg/p2p/preheat/provider/auth/known_handlers.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
// Static handler registry
|
||||||
|
var knownHandlers = map[string]Handler{
|
||||||
|
AuthModeNone: &NoneAuthHandler{},
|
||||||
|
AuthModeBasic: &BasicAuthHandler{&BaseHandler{}},
|
||||||
|
AuthModeCustom: &CustomAuthHandler{&BaseHandler{}},
|
||||||
|
AuthModeOAuth: &TokenAuthHandler{&BaseHandler{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthHandler gets the handler per the mode
|
||||||
|
func GetAuthHandler(mode string) (Handler, bool) {
|
||||||
|
h, ok := knownHandlers[mode]
|
||||||
|
|
||||||
|
return h, ok
|
||||||
|
}
|
24
src/pkg/p2p/preheat/provider/auth/none_handler.go
Normal file
24
src/pkg/p2p/preheat/provider/auth/none_handler.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NoneAuthHandler handles the case of no credentail required.
|
||||||
|
type NoneAuthHandler struct{}
|
||||||
|
|
||||||
|
// Mode implements @Handler.Mode
|
||||||
|
func (nah *NoneAuthHandler) Mode() string {
|
||||||
|
return AuthModeNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorize implements @Handler.Authorize
|
||||||
|
func (nah *NoneAuthHandler) Authorize(req *http.Request, cred *Credential) error {
|
||||||
|
if req == nil {
|
||||||
|
return errors.New("nil request cannot be authorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do nothing
|
||||||
|
return nil
|
||||||
|
}
|
33
src/pkg/p2p/preheat/provider/auth/token_handler.go
Normal file
33
src/pkg/p2p/preheat/provider/auth/token_handler.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenAuthHandler handles the OAuth auth mode.
|
||||||
|
type TokenAuthHandler struct {
|
||||||
|
*BaseHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode implements @Handler.Mode
|
||||||
|
func (t *TokenAuthHandler) Mode() string {
|
||||||
|
return AuthModeOAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorize implements @Handler.Authorize
|
||||||
|
func (t *TokenAuthHandler) Authorize(req *http.Request, cred *Credential) error {
|
||||||
|
if err := t.BaseHandler.Authorize(req, cred); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := cred.Data["token"]; !ok {
|
||||||
|
return errors.New("missing OAuth token")
|
||||||
|
}
|
||||||
|
|
||||||
|
authData := fmt.Sprintf("%s %s", "Bearer", cred.Data["token"])
|
||||||
|
req.Header.Set("Authorization", authData)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
213
src/pkg/p2p/preheat/provider/client/http_client.go
Normal file
213
src/pkg/p2p/preheat/provider/client/http_client.go
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/provider/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
clientTimeout = 10 * time.Second
|
||||||
|
maxIdleConnections = 20
|
||||||
|
idleConnectionTimeout = 30 * time.Second
|
||||||
|
tlsHandshakeTimeout = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultHTTPClient is used as the default http client.
|
||||||
|
var defaultHTTPClient, defaultInsecureHTTPClient *HTTPClient
|
||||||
|
|
||||||
|
// GetHTTPClient returns the singleton HTTP client based on the insecure setting.
|
||||||
|
func GetHTTPClient(insecure bool) *HTTPClient {
|
||||||
|
if insecure {
|
||||||
|
if defaultInsecureHTTPClient == nil {
|
||||||
|
defaultInsecureHTTPClient = NewHTTPClient(insecure)
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultInsecureHTTPClient
|
||||||
|
}
|
||||||
|
|
||||||
|
if defaultHTTPClient == nil {
|
||||||
|
defaultHTTPClient = NewHTTPClient(insecure)
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultHTTPClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPClient help to send http requests
|
||||||
|
type HTTPClient struct {
|
||||||
|
// http client
|
||||||
|
internalClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTTPClient creates a new http client.
|
||||||
|
func NewHTTPClient(insecure bool) *HTTPClient {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: clientTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
MaxIdleConns: maxIdleConnections,
|
||||||
|
IdleConnTimeout: idleConnectionTimeout,
|
||||||
|
TLSHandshakeTimeout: tlsHandshakeTimeout,
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: insecure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HTTPClient{
|
||||||
|
internalClient: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get content from the url
|
||||||
|
func (hc *HTTPClient) Get(url string, cred *auth.Credential, parmas map[string]string, options map[string]string) ([]byte, error) {
|
||||||
|
bytes, err := hc.get(url, cred, parmas, options)
|
||||||
|
logMsg := fmt.Sprintf("Get %s with cred=%v, params=%v, options=%v", url, cred, parmas, options)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%s: %s", logMsg, err)
|
||||||
|
} else {
|
||||||
|
log.Debugf("%s succeed: %s", logMsg, string(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HTTPClient) get(url string, cred *auth.Credential, parmas map[string]string, options map[string]string) ([]byte, error) {
|
||||||
|
if len(url) == 0 {
|
||||||
|
return nil, errors.New("empty url")
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parmas) > 0 {
|
||||||
|
l := []string{}
|
||||||
|
for k, p := range parmas {
|
||||||
|
l = append(l, fmt.Sprintf("%s=%s", k, p))
|
||||||
|
}
|
||||||
|
|
||||||
|
req.URL.RawQuery = strings.Join(l, "&")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(options) > 0 {
|
||||||
|
for k, h := range options {
|
||||||
|
req.Header.Add(k, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Explicitly declare JSON data accepted.
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
// Do auth
|
||||||
|
if err := hc.authorize(req, cred); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := hc.internalClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If failed, read error message; if succeeded, read content.
|
||||||
|
defer res.Body.Close()
|
||||||
|
bytes, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.StatusCode / 100) != 2 {
|
||||||
|
// Return the server error content in the error.
|
||||||
|
return nil, fmt.Errorf("%s '%s' error: %s %s", http.MethodGet, res.Request.URL.String(), res.Status, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post content to the service specified by the url
|
||||||
|
func (hc *HTTPClient) Post(url string, cred *auth.Credential, body interface{}, options map[string]string) ([]byte, error) {
|
||||||
|
bytes, err := hc.post(url, cred, body, options)
|
||||||
|
logMsg := fmt.Sprintf("Post %s with cred=%v, options=%v", url, cred, options)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%s: %s", logMsg, err)
|
||||||
|
} else {
|
||||||
|
log.Debugf("%s succeed: %s", logMsg, string(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HTTPClient) post(url string, cred *auth.Credential, body interface{}, options map[string]string) ([]byte, error) {
|
||||||
|
if len(url) == 0 {
|
||||||
|
return nil, errors.New("empty url")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal body to json data.
|
||||||
|
var bodyContent *strings.Reader
|
||||||
|
if body != nil {
|
||||||
|
content, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("only JSON data supported: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyContent = strings.NewReader(string(content))
|
||||||
|
log.Debugf("POST body: %s", string(content))
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodPost, url, bodyContent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(options) > 0 {
|
||||||
|
for k, h := range options {
|
||||||
|
req.Header.Add(k, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
// Do auth
|
||||||
|
if err := hc.authorize(req, cred); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := hc.internalClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
bytes, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.StatusCode / 100) != 2 {
|
||||||
|
// Return the server error content in the error.
|
||||||
|
return nil, fmt.Errorf("%s '%s' error: %s %s", http.MethodPost, res.Request.URL.String(), res.Status, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HTTPClient) authorize(req *http.Request, cred *auth.Credential) error {
|
||||||
|
if cred != nil {
|
||||||
|
authorizer, ok := auth.GetAuthHandler(cred.Mode)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("no auth handler registered for mode: %s", cred.Mode)
|
||||||
|
}
|
||||||
|
if err := authorizer.Authorize(req, cred); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
101
src/pkg/p2p/preheat/provider/client/http_client_test.go
Normal file
101
src/pkg/p2p/preheat/provider/client/http_client_test.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// 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 client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/provider/auth"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPClientTestSuite is a test suite for testing the HTTP client.
|
||||||
|
type HTTPClientTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
ts *httptest.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTTPClient is the entry of running HttpClientTestSuite.
|
||||||
|
func TestHTTPClient(t *testing.T) {
|
||||||
|
suite.Run(t, &HTTPClientTestSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite prepares the env for the test suite.
|
||||||
|
func (suite *HTTPClientTestSuite) SetupSuite() {
|
||||||
|
suite.ts = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a := r.Header.Get("Authorization")
|
||||||
|
if len(a) == 0 {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-type", "application/json")
|
||||||
|
_, _ = w.Write([]byte("{}"))
|
||||||
|
}))
|
||||||
|
|
||||||
|
suite.ts.StartTLS()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDownSuite clears the env for the test suite.
|
||||||
|
func (suite *HTTPClientTestSuite) TearDownSuite() {
|
||||||
|
suite.ts.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestClientGet tests the client getter method.
|
||||||
|
func (suite *HTTPClientTestSuite) TestClientGet() {
|
||||||
|
c := GetHTTPClient(true)
|
||||||
|
suite.NotNil(c, "get insecure HTTP client")
|
||||||
|
t := c.internalClient.Transport.(*http.Transport)
|
||||||
|
suite.Equal(true, t.TLSClientConfig.InsecureSkipVerify, "InsecureSkipVerify=true")
|
||||||
|
|
||||||
|
c2 := GetHTTPClient(false)
|
||||||
|
suite.NotNil(c2, "get secure HTTP client")
|
||||||
|
t2 := c2.internalClient.Transport.(*http.Transport)
|
||||||
|
suite.Equal(false, t2.TLSClientConfig.InsecureSkipVerify, "InsecureSkipVerify=false")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGet test the Get method
|
||||||
|
func (suite *HTTPClientTestSuite) TestGet() {
|
||||||
|
c := GetHTTPClient(true)
|
||||||
|
suite.NotNil(c, "get insecure HTTP client")
|
||||||
|
|
||||||
|
_, err := c.Get(suite.ts.URL, nil, nil, nil)
|
||||||
|
suite.Error(err, "unauthorized error", err)
|
||||||
|
|
||||||
|
cred := &auth.Credential{
|
||||||
|
Mode: auth.AuthModeBasic,
|
||||||
|
Data: map[string]string{"username": "password"},
|
||||||
|
}
|
||||||
|
data, err := c.Get(suite.ts.URL, cred, map[string]string{"name": "TestGet"}, map[string]string{"Accept": "application/json"})
|
||||||
|
suite.NoError(err, "get data")
|
||||||
|
suite.Equal("{}", string(data), "get json data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPost test the Post method
|
||||||
|
func (suite *HTTPClientTestSuite) TestPost() {
|
||||||
|
c := GetHTTPClient(true)
|
||||||
|
suite.NotNil(c, "get insecure HTTP client")
|
||||||
|
|
||||||
|
cred := &auth.Credential{
|
||||||
|
Mode: auth.AuthModeBasic,
|
||||||
|
Data: map[string]string{"username": "password"},
|
||||||
|
}
|
||||||
|
data, err := c.Post(suite.ts.URL, cred, []byte("{}"), map[string]string{"Accept": "application/json"})
|
||||||
|
suite.NoError(err, "post data")
|
||||||
|
suite.Equal("{}", string(data), "post json data")
|
||||||
|
}
|
141
src/pkg/p2p/preheat/provider/dragonfly.go
Normal file
141
src/pkg/p2p/preheat/provider/dragonfly.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/models/provider"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/provider/auth"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/provider/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
healthCheckEndpoint = "/_ping"
|
||||||
|
preheatEndpoint = "/preheats"
|
||||||
|
preheatTaskEndpoint = "/preheats/{task_id}"
|
||||||
|
dragonflyPending = "WAITING"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dragonflyPreheatCreateResp struct {
|
||||||
|
ID string `json:"ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dragonflyPreheatInfo struct {
|
||||||
|
ID string `json:"ID"`
|
||||||
|
StartTime string `json:"startTime,omitempty"`
|
||||||
|
FinishTime string `json:"finishTime,omitempty"`
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DragonflyDriver implements the provider driver interface for Alibaba dragonfly.
|
||||||
|
// More details, please refer to https://github.com/alibaba/Dragonfly
|
||||||
|
type DragonflyDriver struct {
|
||||||
|
instance *provider.Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Self implements @Driver.Self.
|
||||||
|
func (dd *DragonflyDriver) Self() *Metadata {
|
||||||
|
return &Metadata{
|
||||||
|
ID: "dragonfly",
|
||||||
|
Name: "Dragonfly",
|
||||||
|
Icon: "https://raw.githubusercontent.com/alibaba/Dragonfly/master/docs/images/logo.png",
|
||||||
|
Version: "0.10.1",
|
||||||
|
Source: "https://github.com/alibaba/Dragonfly",
|
||||||
|
Maintainers: []string{"Jin Zhang/taiyun.zj@alibaba-inc.com"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHealth implements @Driver.GetHealth.
|
||||||
|
func (dd *DragonflyDriver) GetHealth() (*DriverStatus, error) {
|
||||||
|
if dd.instance == nil {
|
||||||
|
return nil, errors.New("missing instance metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s%s", strings.TrimSuffix(dd.instance.Endpoint, "/"), healthCheckEndpoint)
|
||||||
|
_, err := client.GetHTTPClient(dd.instance.Insecure).Get(url, dd.getCred(), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
// Unhealthy
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Dragonfly, no error returned means healthy
|
||||||
|
return &DriverStatus{
|
||||||
|
Status: DriverStatusHealthy,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preheat implements @Driver.Preheat.
|
||||||
|
func (dd *DragonflyDriver) Preheat(preheatingImage *PreheatImage) (*PreheatingStatus, error) {
|
||||||
|
if dd.instance == nil {
|
||||||
|
return nil, errors.New("missing instance metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
if preheatingImage == nil {
|
||||||
|
return nil, errors.New("no image specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s%s", strings.TrimSuffix(dd.instance.Endpoint, "/"), preheatEndpoint)
|
||||||
|
bytes, err := client.GetHTTPClient(dd.instance.Insecure).Post(url, dd.getCred(), preheatingImage, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &dragonflyPreheatCreateResp{}
|
||||||
|
if err := json.Unmarshal(bytes, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PreheatingStatus{
|
||||||
|
TaskID: result.ID,
|
||||||
|
Status: provider.PreheatingStatusPending, // default
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckProgress implements @Driver.CheckProgress.
|
||||||
|
func (dd *DragonflyDriver) CheckProgress(taskID string) (*PreheatingStatus, error) {
|
||||||
|
if dd.instance == nil {
|
||||||
|
return nil, errors.New("missing instance metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(taskID) == 0 {
|
||||||
|
return nil, errors.New("no task ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
path := strings.Replace(preheatTaskEndpoint, "{task_id}", taskID, 1)
|
||||||
|
url := fmt.Sprintf("%s%s", strings.TrimSuffix(dd.instance.Endpoint, "/"), path)
|
||||||
|
bytes, err := client.GetHTTPClient(dd.instance.Insecure).Get(url, dd.getCred(), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
status := &dragonflyPreheatInfo{}
|
||||||
|
if err := json.Unmarshal(bytes, status); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Status == dragonflyPending {
|
||||||
|
status.Status = provider.PreheatingStatusPending
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &PreheatingStatus{
|
||||||
|
Status: status.Status,
|
||||||
|
TaskID: taskID,
|
||||||
|
}
|
||||||
|
if status.StartTime != "" {
|
||||||
|
res.StartTime = status.StartTime
|
||||||
|
}
|
||||||
|
if status.FinishTime != "" {
|
||||||
|
res.FinishTime = status.FinishTime
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dd *DragonflyDriver) getCred() *auth.Credential {
|
||||||
|
return &auth.Credential{
|
||||||
|
Mode: dd.instance.AuthMode,
|
||||||
|
Data: dd.instance.AuthInfo,
|
||||||
|
}
|
||||||
|
}
|
156
src/pkg/p2p/preheat/provider/dragonfly_test.go
Normal file
156
src/pkg/p2p/preheat/provider/dragonfly_test.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
// 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 provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/models/provider"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/provider/auth"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DragonflyTestSuite is a test suite of testing Dragonfly driver.
|
||||||
|
type DragonflyTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
dragonfly *httptest.Server
|
||||||
|
driver *DragonflyDriver
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDragonfly is the entry method of running DragonflyTestSuite.
|
||||||
|
func TestDragonfly(t *testing.T) {
|
||||||
|
suite.Run(t, &DragonflyTestSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite prepares the env for DragonflyTestSuite.
|
||||||
|
func (suite *DragonflyTestSuite) SetupSuite() {
|
||||||
|
suite.dragonfly = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.RequestURI {
|
||||||
|
case healthCheckEndpoint:
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
case preheatEndpoint:
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, _ = w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
image := &PreheatImage{}
|
||||||
|
if err := json.Unmarshal(data, image); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, _ = w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if image.Type == "image" &&
|
||||||
|
image.URL == "https://harbor.com" &&
|
||||||
|
image.ImageName == "busybox" &&
|
||||||
|
image.Tag == "latest" {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte(`{"ID":"dragonfly-id"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
case strings.Replace(preheatTaskEndpoint, "{task_id}", "dragonfly-id", 1):
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status := &dragonflyPreheatInfo{
|
||||||
|
ID: "dragonfly-id",
|
||||||
|
StartTime: time.Now().UTC().String(),
|
||||||
|
FinishTime: time.Now().Add(5 * time.Minute).UTC().String(),
|
||||||
|
Status: "SUCCESS",
|
||||||
|
}
|
||||||
|
bytes, _ := json.Marshal(status)
|
||||||
|
_, _ = w.Write(bytes)
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
suite.dragonfly.StartTLS()
|
||||||
|
|
||||||
|
suite.driver = &DragonflyDriver{
|
||||||
|
instance: &provider.Instance{
|
||||||
|
ID: 1,
|
||||||
|
Name: "test-instance",
|
||||||
|
Vendor: DriverDragonfly,
|
||||||
|
Endpoint: suite.dragonfly.URL,
|
||||||
|
AuthMode: auth.AuthModeNone,
|
||||||
|
Enabled: true,
|
||||||
|
Default: true,
|
||||||
|
Insecure: true,
|
||||||
|
Status: DriverStatusHealthy,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDownSuite clears the env for DragonflyTestSuite.
|
||||||
|
func (suite *DragonflyTestSuite) TearDownSuite() {
|
||||||
|
suite.dragonfly.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSelf tests Self method.
|
||||||
|
func (suite *DragonflyTestSuite) TestSelf() {
|
||||||
|
m := suite.driver.Self()
|
||||||
|
suite.Equal(DriverDragonfly, m.ID, "self metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetHealth tests GetHealth method.
|
||||||
|
func (suite *DragonflyTestSuite) TestGetHealth() {
|
||||||
|
st, err := suite.driver.GetHealth()
|
||||||
|
require.NoError(suite.T(), err, "get health")
|
||||||
|
suite.Equal(DriverStatusHealthy, st.Status, "healthy status")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPreheat tests Preheat method.
|
||||||
|
func (suite *DragonflyTestSuite) TestPreheat() {
|
||||||
|
st, err := suite.driver.Preheat(&PreheatImage{
|
||||||
|
Type: "image",
|
||||||
|
ImageName: "busybox",
|
||||||
|
Tag: "latest",
|
||||||
|
URL: "https://harbor.com",
|
||||||
|
})
|
||||||
|
require.NoError(suite.T(), err, "preheat image")
|
||||||
|
suite.Equal("dragonfly-id", st.TaskID, "preheat image result")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckProgress tests CheckProgress method.
|
||||||
|
func (suite *DragonflyTestSuite) TestCheckProgress() {
|
||||||
|
st, err := suite.driver.CheckProgress("dragonfly-id")
|
||||||
|
require.NoError(suite.T(), err, "get preheat status")
|
||||||
|
suite.Equal(provider.PreheatingStatusSuccess, st.Status, "preheat status")
|
||||||
|
}
|
78
src/pkg/p2p/preheat/provider/driver.go
Normal file
78
src/pkg/p2p/preheat/provider/driver.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DriverStatusHealthy represents the healthy status
|
||||||
|
DriverStatusHealthy = "Healthy"
|
||||||
|
|
||||||
|
// DriverStatusUnHealthy represents the unhealthy status
|
||||||
|
DriverStatusUnHealthy = "Unhealthy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Driver defines the capabilities one distribution provider should have.
|
||||||
|
// Includes:
|
||||||
|
// Self descriptor
|
||||||
|
// Health checking
|
||||||
|
// Preheat related : Preheat means transfer the preheating image to the network of distribution provider in advance.
|
||||||
|
type Driver interface {
|
||||||
|
// Self returns the metadata of the driver.
|
||||||
|
// The metadata includes: name, icon(optional), maintainers(optional), version and source repo.
|
||||||
|
Self() *Metadata
|
||||||
|
|
||||||
|
// Try to get the health status of the driver.
|
||||||
|
// If succeed, a non nil status object will be returned;
|
||||||
|
// otherwise, a non nil error will be set.
|
||||||
|
GetHealth() (*DriverStatus, error)
|
||||||
|
|
||||||
|
// Preheat the specified image
|
||||||
|
// If succeed, a non nil result object with preheating task id will be returned;
|
||||||
|
// otherwise, a non nil error will be set.
|
||||||
|
Preheat(preheatingImage *PreheatImage) (*PreheatingStatus, error)
|
||||||
|
|
||||||
|
// Check the progress of the preheating process.
|
||||||
|
// If succeed, a non nil status object with preheating status will be returned;
|
||||||
|
// otherwise, a non nil error will be set.
|
||||||
|
CheckProgress(taskID string) (*PreheatingStatus, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata contains the basic information of the provider.
|
||||||
|
type Metadata struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Icon string `json:"icon,omitempty"`
|
||||||
|
Maintainers []string `json:"maintainers,omitempty"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DriverStatus keeps the health status of driver.
|
||||||
|
type DriverStatus struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreheatImage contains related information which can help providers to get/pull the images.
|
||||||
|
type PreheatImage struct {
|
||||||
|
// The image content type, only support 'image' now
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
// The access URL of the preheating image
|
||||||
|
URL string `json:"url"`
|
||||||
|
|
||||||
|
// The headers which will be sent to the above URL of preheating image
|
||||||
|
Headers map[string]interface{} `json:"headers"`
|
||||||
|
|
||||||
|
// The image name
|
||||||
|
ImageName string `json:"image,omitempty"`
|
||||||
|
|
||||||
|
// The tag
|
||||||
|
Tag string `json:"tag,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreheatingStatus contains the related results/status of the preheating operation
|
||||||
|
// from the provider.
|
||||||
|
type PreheatingStatus struct {
|
||||||
|
TaskID string `json:"task_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
StartTime string `json:"start_time"`
|
||||||
|
FinishTime string `json:"finish_time"`
|
||||||
|
}
|
18
src/pkg/p2p/preheat/provider/factory.go
Normal file
18
src/pkg/p2p/preheat/provider/factory.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/models/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Factory is responsible to create a new driver based on the metadata.
|
||||||
|
type Factory func(instance *provider.Instance) (Driver, error)
|
||||||
|
|
||||||
|
// DragonflyFactory creates dragonfly driver
|
||||||
|
func DragonflyFactory(instance *provider.Instance) (Driver, error) {
|
||||||
|
return &DragonflyDriver{instance}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KrakenFactory creates kraken driver
|
||||||
|
func KrakenFactory(instance *provider.Instance) (Driver, error) {
|
||||||
|
return &KrakenDriver{instance, nil}, nil
|
||||||
|
}
|
51
src/pkg/p2p/preheat/provider/known_drivers.go
Normal file
51
src/pkg/p2p/preheat/provider/known_drivers.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DriverDragonfly represents the driver for dragonfly
|
||||||
|
DriverDragonfly = "dragonfly"
|
||||||
|
// DriverKraken represents the driver for kraken
|
||||||
|
DriverKraken = "kraken"
|
||||||
|
)
|
||||||
|
|
||||||
|
// knownDrivers is static driver Factory registry
|
||||||
|
var knownDrivers = map[string]Factory{
|
||||||
|
DriverDragonfly: DragonflyFactory,
|
||||||
|
DriverKraken: KrakenFactory,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListProviders returns all the registered drivers.
|
||||||
|
func ListProviders() ([]*Metadata, error) {
|
||||||
|
results := make([]*Metadata, 0)
|
||||||
|
|
||||||
|
for _, f := range knownDrivers {
|
||||||
|
drv, err := f(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, drv.Self())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort results
|
||||||
|
if len(results) > 1 {
|
||||||
|
sort.SliceIsSorted(results, func(i, j int) bool {
|
||||||
|
return strings.Compare(results[i].ID, results[j].ID) > 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProvider returns the driver factory identified by the ID.
|
||||||
|
//
|
||||||
|
// If exists, bool flag will be set to be true and a non-nil reference will be returned.
|
||||||
|
func GetProvider(ID string) (Factory, bool) {
|
||||||
|
f, ok := knownDrivers[ID]
|
||||||
|
|
||||||
|
return f, ok
|
||||||
|
}
|
33
src/pkg/p2p/preheat/provider/known_drivers_test.go
Normal file
33
src/pkg/p2p/preheat/provider/known_drivers_test.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KnownDriverTestSuite is a test suite of testing known driver related.
|
||||||
|
type KnownDriverTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestKnownDriver is the entry of running KnownDriverTestSuite.
|
||||||
|
func TestKnownDriver(t *testing.T) {
|
||||||
|
suite.Run(t, &KnownDriverTestSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KnownDriverTestSuite) TestListProviders() {
|
||||||
|
metadata, err := ListProviders()
|
||||||
|
require.NoError(suite.T(), err, "list providers")
|
||||||
|
suite.Equal(len(knownDrivers), len(metadata))
|
||||||
|
suite.Equal(DriverDragonfly, metadata[0].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KnownDriverTestSuite) TestGetProvider() {
|
||||||
|
f, ok := GetProvider(DriverDragonfly)
|
||||||
|
require.Equal(suite.T(), true, ok)
|
||||||
|
|
||||||
|
_, err := f(nil)
|
||||||
|
suite.NoError(err, "dragonfly factory")
|
||||||
|
}
|
137
src/pkg/p2p/preheat/provider/kraken.go
Normal file
137
src/pkg/p2p/preheat/provider/kraken.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/distribution/manifest/schema2"
|
||||||
|
cm "github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/models/provider"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/provider/auth"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/provider/client"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
krakenHealthPath = "/health"
|
||||||
|
krakenPreheatPath = "/registry/notifications"
|
||||||
|
)
|
||||||
|
|
||||||
|
type digestFetcherFunc func(repoName, tag string) (string, error)
|
||||||
|
|
||||||
|
// KrakenDriver implements the provider driver interface for Uber kraken.
|
||||||
|
// More details, please refer to https://github.com/uber/kraken
|
||||||
|
type KrakenDriver struct {
|
||||||
|
instance *provider.Instance
|
||||||
|
digestFetcher digestFetcherFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Self implements @Driver.Self.
|
||||||
|
func (kd *KrakenDriver) Self() *Metadata {
|
||||||
|
return &Metadata{
|
||||||
|
ID: "kraken",
|
||||||
|
Name: "Kraken",
|
||||||
|
Icon: "https://github.com/uber/kraken/blob/master/assets/kraken-logo-color.svg",
|
||||||
|
Version: "0.1.3",
|
||||||
|
Source: "https://github.com/uber/kraken",
|
||||||
|
Maintainers: []string{"mmpei/peimingming@corp.netease.com"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHealth implements @Driver.GetHealth.
|
||||||
|
func (kd *KrakenDriver) GetHealth() (*DriverStatus, error) {
|
||||||
|
if kd.instance == nil {
|
||||||
|
return nil, errors.New("missing instance metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s%s", strings.TrimSuffix(kd.instance.Endpoint, "/"), krakenHealthPath)
|
||||||
|
_, err := client.GetHTTPClient(kd.instance.Insecure).Get(url, kd.getCred(), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
// Unhealthy
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Kraken, no error returned means healthy
|
||||||
|
return &DriverStatus{
|
||||||
|
Status: DriverStatusHealthy,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preheat implements @Driver.Preheat.
|
||||||
|
func (kd *KrakenDriver) Preheat(preheatingImage *PreheatImage) (*PreheatingStatus, error) {
|
||||||
|
if kd.instance == nil {
|
||||||
|
return nil, errors.New("missing instance metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
if preheatingImage == nil {
|
||||||
|
return nil, errors.New("no image specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s%s", strings.TrimSuffix(kd.instance.Endpoint, "/"), krakenPreheatPath)
|
||||||
|
var events = make([]cm.Event, 0)
|
||||||
|
eventID := utils.GenerateRandomString()
|
||||||
|
if kd.digestFetcher == nil {
|
||||||
|
kd.digestFetcher = fetchDigest
|
||||||
|
}
|
||||||
|
digest, err := kd.digestFetcher(preheatingImage.ImageName, preheatingImage.Tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
event := cm.Event{
|
||||||
|
ID: eventID,
|
||||||
|
TimeStamp: time.Now().UTC(),
|
||||||
|
Action: "push",
|
||||||
|
Target: &cm.Target{
|
||||||
|
MediaType: schema2.MediaTypeManifest,
|
||||||
|
Digest: digest,
|
||||||
|
Repository: preheatingImage.ImageName,
|
||||||
|
URL: preheatingImage.URL,
|
||||||
|
Tag: preheatingImage.Tag,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
events = append(events, event)
|
||||||
|
var payload = cm.Notification{
|
||||||
|
Events: events,
|
||||||
|
}
|
||||||
|
_, err = client.GetHTTPClient(kd.instance.Insecure).Post(url, kd.getCred(), payload, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PreheatingStatus{
|
||||||
|
TaskID: eventID,
|
||||||
|
Status: provider.PreheatingStatusSuccess,
|
||||||
|
FinishTime: time.Now().String(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckProgress implements @Driver.CheckProgress.
|
||||||
|
// TODO: This should be improved later
|
||||||
|
func (kd *KrakenDriver) CheckProgress(taskID string) (*PreheatingStatus, error) {
|
||||||
|
return &PreheatingStatus{
|
||||||
|
TaskID: taskID,
|
||||||
|
Status: provider.PreheatingStatusSuccess,
|
||||||
|
FinishTime: time.Now().String(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kd *KrakenDriver) getCred() *auth.Credential {
|
||||||
|
return &auth.Credential{
|
||||||
|
Mode: kd.instance.AuthMode,
|
||||||
|
Data: kd.instance.AuthInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchDigest(repoName, tag string) (string, error) {
|
||||||
|
exist, digest, err := registry.Cli.ManifestExist(repoName, tag)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !exist {
|
||||||
|
return "", errors.New("image not found")
|
||||||
|
}
|
||||||
|
return digest, nil
|
||||||
|
}
|
146
src/pkg/p2p/preheat/provider/kraken_test.go
Normal file
146
src/pkg/p2p/preheat/provider/kraken_test.go
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
// 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 provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
cm "github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/models/provider"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/provider/auth"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KrakenTestSuite is a test suite of testing Kraken driver.
|
||||||
|
type KrakenTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
kraken *httptest.Server
|
||||||
|
driver *KrakenDriver
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestKraken is the entry method of running KrakenTestSuite.
|
||||||
|
func TestKraken(t *testing.T) {
|
||||||
|
suite.Run(t, &KrakenTestSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite prepares the env for KrakenTestSuite.
|
||||||
|
func (suite *KrakenTestSuite) SetupSuite() {
|
||||||
|
suite.kraken = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.RequestURI {
|
||||||
|
case krakenHealthPath:
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
case krakenPreheatPath:
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, _ = w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = &cm.Notification{
|
||||||
|
Events: []cm.Event{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, payload); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, _ = w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(payload.Events) > 0 {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
suite.kraken.StartTLS()
|
||||||
|
|
||||||
|
suite.driver = &KrakenDriver{
|
||||||
|
instance: &provider.Instance{
|
||||||
|
ID: 2,
|
||||||
|
Name: "test-instance2",
|
||||||
|
Vendor: DriverKraken,
|
||||||
|
Endpoint: suite.kraken.URL,
|
||||||
|
AuthMode: auth.AuthModeNone,
|
||||||
|
Enabled: true,
|
||||||
|
Default: true,
|
||||||
|
Insecure: true,
|
||||||
|
Status: DriverStatusHealthy,
|
||||||
|
},
|
||||||
|
digestFetcher: func(repoName, tag string) (s string, e error) {
|
||||||
|
return "image@digest", nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDownSuite clears the env for KrakenTestSuite.
|
||||||
|
func (suite *KrakenTestSuite) TearDownSuite() {
|
||||||
|
suite.kraken.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSelf tests Self method.
|
||||||
|
func (suite *KrakenTestSuite) TestSelf() {
|
||||||
|
m := suite.driver.Self()
|
||||||
|
suite.Equal(DriverKraken, m.ID, "self metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetHealth tests GetHealth method.
|
||||||
|
func (suite *KrakenTestSuite) TestGetHealth() {
|
||||||
|
st, err := suite.driver.GetHealth()
|
||||||
|
require.NoError(suite.T(), err, "get health")
|
||||||
|
suite.Equal(DriverStatusHealthy, st.Status, "healthy status")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPreheat tests Preheat method.
|
||||||
|
func (suite *KrakenTestSuite) TestPreheat() {
|
||||||
|
st, err := suite.driver.Preheat(&PreheatImage{
|
||||||
|
Type: "image",
|
||||||
|
ImageName: "busybox",
|
||||||
|
Tag: "latest",
|
||||||
|
URL: "https://harbor.com",
|
||||||
|
})
|
||||||
|
require.NoError(suite.T(), err, "preheat image")
|
||||||
|
suite.Equal(provider.PreheatingStatusSuccess, st.Status, "preheat image result")
|
||||||
|
suite.NotEmptyf(st.FinishTime, "finish time")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckProgress tests CheckProgress method.
|
||||||
|
func (suite *KrakenTestSuite) TestCheckProgress() {
|
||||||
|
st, err := suite.driver.CheckProgress("kraken-id")
|
||||||
|
require.NoError(suite.T(), err, "get preheat status")
|
||||||
|
suite.Equal(provider.PreheatingStatusSuccess, st.Status, "preheat status")
|
||||||
|
suite.NotEmptyf(st.FinishTime, "finish time")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user