mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-18 22:57:38 +01:00
Merge pull request #12331 from steven-zou/feat/p2p_preheat_drivers
feat(preheat):add preheat drivers
This commit is contained in:
commit
309a9ff13c
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