mirror of
https://github.com/goharbor/harbor.git
synced 2024-09-29 05:47:31 +02:00
Tecent TCR Provider
1. Docker image registry. 2. Helm chart registry. Signed-off-by: 疯魔慕薇 <kfanjian@gmail.com> Signed-off-by: fanjiankong <fanjiankong@tencent.com>
This commit is contained in:
parent
ec2f251d63
commit
9f8a743da9
@ -66,6 +66,7 @@ require (
|
|||||||
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect
|
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect
|
||||||
github.com/spf13/viper v1.4.0 // indirect
|
github.com/spf13/viper v1.4.0 // indirect
|
||||||
github.com/stretchr/testify v1.5.1
|
github.com/stretchr/testify v1.5.1
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go v1.0.62
|
||||||
github.com/theupdateframework/notary v0.6.1
|
github.com/theupdateframework/notary v0.6.1
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344
|
||||||
|
@ -766,6 +766,8 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H
|
|||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||||
github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
|
github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go v1.0.62 h1:Vnr3IqaafEuQUciG6D6EaeLJm26Mg8sjAfbI4OoeauM=
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go v1.0.62/go.mod h1:asUz5BPXxgoPGaRgZaVm1iGcUAuHyYUo1nXqKa83cvI=
|
||||||
github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0=
|
github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0=
|
||||||
github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY=
|
github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY=
|
||||||
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
|
@ -54,6 +54,8 @@ import (
|
|||||||
_ "github.com/goharbor/harbor/src/replication/adapter/dtr"
|
_ "github.com/goharbor/harbor/src/replication/adapter/dtr"
|
||||||
// register the Artifact Hub adapter
|
// register the Artifact Hub adapter
|
||||||
_ "github.com/goharbor/harbor/src/replication/adapter/artifacthub"
|
_ "github.com/goharbor/harbor/src/replication/adapter/artifacthub"
|
||||||
|
// register the TencentCloud TCR adapter
|
||||||
|
_ "github.com/goharbor/harbor/src/replication/adapter/tencentcr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Replication implements the job interface
|
// Replication implements the job interface
|
||||||
|
233
src/replication/adapter/tencentcr/adapter.go
Normal file
233
src/replication/adapter/tencentcr/adapter.go
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
package tencentcr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||||
|
commonhttp "github.com/goharbor/harbor/src/common/http"
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/registry/auth/bearer"
|
||||||
|
adp "github.com/goharbor/harbor/src/replication/adapter"
|
||||||
|
"github.com/goharbor/harbor/src/replication/adapter/native"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
"github.com/goharbor/harbor/src/replication/util"
|
||||||
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
||||||
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
||||||
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/regions"
|
||||||
|
tcr "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tcr/v20190924"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errInvalidTcrEndpoint error = errors.New("[tencent-tcr.newAdapter] Invalid TCR instance endpoint")
|
||||||
|
errPingTcrEndpointFailed error = errors.New("[tencent-tcr.newAdapter] Ping TCR instance endpoint failed")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if err := adp.RegisterFactory(model.RegistryTypeTencentTcr, new(factory)); err != nil {
|
||||||
|
log.Errorf("failed to register factory for %s: %v", model.RegistryTypeTencentTcr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Infof("the factory for adapter %s registered", model.RegistryTypeTencentTcr)
|
||||||
|
}
|
||||||
|
|
||||||
|
type factory struct{}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement Factory Interface
|
||||||
|
**/
|
||||||
|
var _ adp.Factory = &factory{}
|
||||||
|
|
||||||
|
// Create ...
|
||||||
|
func (f *factory) Create(r *model.Registry) (adp.Adapter, error) {
|
||||||
|
return newAdapter(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdapterPattern ...
|
||||||
|
func (f *factory) AdapterPattern() *model.AdapterPattern {
|
||||||
|
return getAdapterInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAdapterInfo() *model.AdapterPattern {
|
||||||
|
return &model.AdapterPattern{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type adapter struct {
|
||||||
|
*native.Adapter
|
||||||
|
registryID *string
|
||||||
|
regionName *string
|
||||||
|
tcrClient *tcr.Client
|
||||||
|
pageSize *int64
|
||||||
|
client *commonhttp.Client
|
||||||
|
registry *model.Registry
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement Adapter Interface
|
||||||
|
**/
|
||||||
|
var _ adp.Adapter = &adapter{}
|
||||||
|
|
||||||
|
func newAdapter(registry *model.Registry) (a *adapter, err error) {
|
||||||
|
if !isSecretID(registry.Credential.AccessKey) {
|
||||||
|
err = errors.New("[tencent-tcr.newAdapter] Please use SecretId/SecretKey, NOT docker login Username/Password")
|
||||||
|
log.Debugf("[tencent-tcr.newAdapter] error=%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query TCR instance info via endpoint.
|
||||||
|
var registryURL *url.URL
|
||||||
|
registryURL, _ = url.Parse(registry.URL)
|
||||||
|
|
||||||
|
if strings.Index(registryURL.Host, ".tencentcloudcr.com") < 0 {
|
||||||
|
log.Errorf("[tencent-tcr.newAdapter] errInvalidTcrEndpoint=%v", err)
|
||||||
|
return nil, errInvalidTcrEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
realm, service, err := ping(registry)
|
||||||
|
log.Debugf("[tencent-tcr.newAdapter] realm=%s, service=%s error=%v", realm, service, err)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("[tencent-tcr.newAdapter] ping failed. error=%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create TCR API client
|
||||||
|
var tcrCredential = common.NewCredential(registry.Credential.AccessKey, registry.Credential.AccessSecret)
|
||||||
|
var cfp = profile.NewClientProfile()
|
||||||
|
var client *tcr.Client
|
||||||
|
// temp client used to get TCR instance info
|
||||||
|
client, err = tcr.NewClient(tcrCredential, regions.Guangzhou, cfp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req = tcr.NewDescribeInstancesRequest()
|
||||||
|
req.AllRegion = common.BoolPtr(true)
|
||||||
|
req.Filters = []*tcr.Filter{
|
||||||
|
{
|
||||||
|
Name: common.StringPtr("RegistryName"),
|
||||||
|
Values: []*string{common.StringPtr(strings.ReplaceAll(registryURL.Host, ".tencentcloudcr.com", ""))},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var resp = tcr.NewDescribeInstancesResponse()
|
||||||
|
resp, err = client.DescribeInstances(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("DescribeInstances error=%s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if *resp.Response.TotalCount == 0 {
|
||||||
|
err = fmt.Errorf("[tencent-tcr.newAdapter] Can not get TCR instance info. RequestId=%s", *resp.Response.RequestId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var instanceInfo = resp.Response.Registries[0]
|
||||||
|
log.Debugf("[tencent-tcr.InstanceInfo] registry.URL=%s, host=%s, PublicDomain=%s, RegionName=%s, RegistryId=%s",
|
||||||
|
registry.URL, registryURL.Host, *instanceInfo.PublicDomain, *instanceInfo.RegionName, *instanceInfo.RegistryId)
|
||||||
|
|
||||||
|
// rebuild TCR SDK client
|
||||||
|
client, err = tcr.NewClient(tcrCredential, *instanceInfo.RegionName, cfp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var credential = NewAuth(instanceInfo.RegistryId, client)
|
||||||
|
var transport = util.GetHTTPTransport(registry.Insecure)
|
||||||
|
var authorizer = bearer.NewAuthorizer(realm, service, credential, transport)
|
||||||
|
|
||||||
|
return &adapter{
|
||||||
|
registry: registry,
|
||||||
|
registryID: instanceInfo.RegistryId,
|
||||||
|
regionName: instanceInfo.RegionName,
|
||||||
|
tcrClient: client,
|
||||||
|
pageSize: common.Int64Ptr(20),
|
||||||
|
client: commonhttp.NewClient(
|
||||||
|
&http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
},
|
||||||
|
credential,
|
||||||
|
),
|
||||||
|
Adapter: native.NewAdapterWithAuthorizer(registry, authorizer),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ping(registry *model.Registry) (string, string, error) {
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: util.GetHTTPTransport(registry.Insecure),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Get(registry.URL + "/v2/")
|
||||||
|
log.Debugf("[tencent-tcr.ping] error=%v", err)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
challenges := challenge.ResponseChallenges(resp)
|
||||||
|
for _, challenge := range challenges {
|
||||||
|
if challenge.Scheme == "bearer" {
|
||||||
|
return challenge.Parameters["realm"], challenge.Parameters["service"], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", "", fmt.Errorf("[tencent-tcr.ping] bearer auth scheme isn't supported: %v", challenges)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) Info() (info *model.RegistryInfo, err error) {
|
||||||
|
info = &model.RegistryInfo{
|
||||||
|
Type: model.RegistryTypeTencentTcr,
|
||||||
|
SupportedResourceTypes: []model.ResourceType{
|
||||||
|
model.ResourceTypeImage,
|
||||||
|
model.ResourceTypeChart,
|
||||||
|
},
|
||||||
|
SupportedResourceFilters: []*model.FilterStyle{
|
||||||
|
{
|
||||||
|
Type: model.FilterTypeName,
|
||||||
|
Style: model.FilterStyleTypeText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: model.FilterTypeTag,
|
||||||
|
Style: model.FilterStyleTypeText,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SupportedTriggers: []model.TriggerType{
|
||||||
|
model.TriggerTypeManual,
|
||||||
|
model.TriggerTypeScheduled,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) PrepareForPush(resources []*model.Resource) (err error) {
|
||||||
|
log.Debugf("[tencent-tcr.PrepareForPush]")
|
||||||
|
for _, resource := range resources {
|
||||||
|
if resource == nil {
|
||||||
|
return errors.New("the resource cannot be null")
|
||||||
|
}
|
||||||
|
if resource.Metadata == nil {
|
||||||
|
return errors.New("[tencent-tcr.PrepareForPush] the metadata of resource cannot be null")
|
||||||
|
}
|
||||||
|
if resource.Metadata.Repository == nil {
|
||||||
|
return errors.New("[tencent-tcr.PrepareForPush] the namespace of resource cannot be null")
|
||||||
|
}
|
||||||
|
if len(resource.Metadata.Repository.Name) == 0 {
|
||||||
|
return errors.New("[tencent-tcr.PrepareForPush] the name of the namespace cannot be null")
|
||||||
|
}
|
||||||
|
var paths = strings.Split(resource.Metadata.Repository.Name, "/")
|
||||||
|
var namespace = paths[0]
|
||||||
|
var repository = path.Join(paths[1:]...)
|
||||||
|
|
||||||
|
log.Debugf("[tencent-tcr.PrepareForPush.createPrivateNamespace] namespace=%s", namespace)
|
||||||
|
err = a.createPrivateNamespace(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debugf("[tencent-tcr.PrepareForPush.createRepository] namespace=%s, repository=%s", namespace, repository)
|
||||||
|
err = a.createRepository(namespace, repository)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
303
src/replication/adapter/tencentcr/adapter_test.go
Normal file
303
src/replication/adapter/tencentcr/adapter_test.go
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
package tencentcr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/test"
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
adp "github.com/goharbor/harbor/src/replication/adapter"
|
||||||
|
"github.com/goharbor/harbor/src/replication/adapter/native"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
|
||||||
|
tcr "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tcr/v20190924"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mockAccessKey = "AKIDxxxx"
|
||||||
|
mockAccessSecret = "xxxxx"
|
||||||
|
tcrClient *tcr.Client
|
||||||
|
)
|
||||||
|
|
||||||
|
func setup() {
|
||||||
|
if ak := os.Getenv("TENCENT_AK"); ak != "" {
|
||||||
|
log.Info("USE AK from ENV")
|
||||||
|
mockAccessKey = ak
|
||||||
|
}
|
||||||
|
if sk := os.Getenv("TENCENT_SK"); sk != "" {
|
||||||
|
log.Info("USE SK from ENV")
|
||||||
|
mockAccessSecret = sk
|
||||||
|
}
|
||||||
|
// var tcrCredential = common.NewCredential(mockAccessKey, mockAccessSecret)
|
||||||
|
// var cfp = profile.NewClientProfile()
|
||||||
|
|
||||||
|
// tcrClient, _ = tcr.NewClient(tcrCredential, regions.Guangzhou, cfp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func teardown() {}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
setup()
|
||||||
|
code := m.Run()
|
||||||
|
teardown()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdapter_NewAdapter(t *testing.T) {
|
||||||
|
factory, err := adp.GetFactory("BadName")
|
||||||
|
assert.Nil(t, factory)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
factory, err = adp.GetFactory(model.RegistryTypeTencentTcr)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, factory)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdapter_NewAdapter_NilAKSK(t *testing.T) {
|
||||||
|
// Nil AK/SK
|
||||||
|
adapter, err := newAdapter(&model.Registry{
|
||||||
|
Type: model.RegistryTypeTencentTcr,
|
||||||
|
Credential: &model.Credential{},
|
||||||
|
})
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Nil(t, adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdapter_NewAdapter_InvalidEndpoint(t *testing.T) {
|
||||||
|
// Invaild endpoint
|
||||||
|
adapter, err := newAdapter(&model.Registry{
|
||||||
|
Type: model.RegistryTypeTencentTcr,
|
||||||
|
Credential: &model.Credential{
|
||||||
|
AccessKey: mockAccessKey,
|
||||||
|
AccessSecret: mockAccessSecret,
|
||||||
|
},
|
||||||
|
URL: "$$$",
|
||||||
|
})
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.EqualError(t, err, errInvalidTcrEndpoint.Error())
|
||||||
|
assert.Nil(t, adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdapter_NewAdapter_Pingfailed(t *testing.T) {
|
||||||
|
// Invaild endpoint
|
||||||
|
adapter, err := newAdapter(&model.Registry{
|
||||||
|
Type: model.RegistryTypeTencentTcr,
|
||||||
|
Credential: &model.Credential{
|
||||||
|
AccessKey: mockAccessKey,
|
||||||
|
AccessSecret: mockAccessSecret,
|
||||||
|
},
|
||||||
|
URL: "https://.tencentcloudcr.com",
|
||||||
|
})
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Nil(t, adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdapter_NewAdapter_InvalidAKSK(t *testing.T) {
|
||||||
|
// Error AK/SK
|
||||||
|
adapter, err := newAdapter(&model.Registry{
|
||||||
|
Type: model.RegistryTypeTencentTcr,
|
||||||
|
Credential: &model.Credential{
|
||||||
|
AccessKey: "mockAccessKey",
|
||||||
|
AccessSecret: "mockAccessSecret",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Nil(t, adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdapter_NewAdapter_Ok(t *testing.T) {
|
||||||
|
adapter, err := newAdapter(&model.Registry{
|
||||||
|
Type: model.RegistryTypeTencentTcr,
|
||||||
|
Credential: &model.Credential{
|
||||||
|
AccessKey: mockAccessKey,
|
||||||
|
AccessSecret: mockAccessSecret,
|
||||||
|
},
|
||||||
|
URL: "https://harbor-community.tencentcloudcr.com",
|
||||||
|
})
|
||||||
|
if sdkerr, ok := err.(*errors.TencentCloudSDKError); ok {
|
||||||
|
log.Infof("sdk error, error=%v", sdkerr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NotNil(t, adapter)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdapter_NewAdapter_InsecureOk(t *testing.T) {
|
||||||
|
adapter, err := newAdapter(&model.Registry{
|
||||||
|
Type: model.RegistryTypeTencentTcr,
|
||||||
|
Credential: &model.Credential{
|
||||||
|
AccessKey: mockAccessKey,
|
||||||
|
AccessSecret: mockAccessSecret,
|
||||||
|
},
|
||||||
|
Insecure: true,
|
||||||
|
URL: "https://harbor-community.tencentcloudcr.com",
|
||||||
|
})
|
||||||
|
if sdkerr, ok := err.(*errors.TencentCloudSDKError); ok {
|
||||||
|
log.Infof("sdk error, error=%v", sdkerr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NotNil(t, adapter)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMockAdapter(t *testing.T, hasCred, health bool) (*adapter, *httptest.Server) {
|
||||||
|
server := test.NewServer(
|
||||||
|
&test.RequestHandlerMapping{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Pattern: "/v2/_catalog",
|
||||||
|
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`
|
||||||
|
{
|
||||||
|
"repositories": [
|
||||||
|
"test1"
|
||||||
|
]
|
||||||
|
}`))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&test.RequestHandlerMapping{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Pattern: "/v2/{repo}/tags/list",
|
||||||
|
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`
|
||||||
|
{
|
||||||
|
"name": "test1",
|
||||||
|
"tags": [
|
||||||
|
"latest"
|
||||||
|
]
|
||||||
|
}`))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&test.RequestHandlerMapping{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Pattern: "/v2/",
|
||||||
|
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Println(r.Method, r.URL)
|
||||||
|
if health {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&test.RequestHandlerMapping{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Pattern: "/",
|
||||||
|
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Println(r.Method, r.URL)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&test.RequestHandlerMapping{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Pattern: "/",
|
||||||
|
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Println(r.Method, r.URL)
|
||||||
|
if buf, e := ioutil.ReadAll(&io.LimitedReader{R: r.Body, N: 80}); e == nil {
|
||||||
|
fmt.Println("\t", string(buf))
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
registry := &model.Registry{
|
||||||
|
Type: model.RegistryTypeAwsEcr,
|
||||||
|
URL: server.URL,
|
||||||
|
}
|
||||||
|
if hasCred {
|
||||||
|
registry.Credential = &model.Credential{
|
||||||
|
AccessKey: "AKIDxxxx",
|
||||||
|
AccessSecret: "abcdefg",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &adapter{
|
||||||
|
registry: registry,
|
||||||
|
Adapter: native.NewAdapter(registry),
|
||||||
|
}, server
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdapter_Info(t *testing.T) {
|
||||||
|
tcrAdapter, _ := getMockAdapter(t, true, true)
|
||||||
|
info, err := tcrAdapter.Info()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdapter_PrepareForPush(t *testing.T) {
|
||||||
|
a, s := getMockAdapter(t, true, true)
|
||||||
|
defer s.Close()
|
||||||
|
resources := []*model.Resource{
|
||||||
|
{
|
||||||
|
Type: model.ResourceTypeImage,
|
||||||
|
Metadata: &model.ResourceMetadata{
|
||||||
|
Repository: &model.Repository{
|
||||||
|
Name: "busybox",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.PrepareForPush(resources)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdapter_PrepareForPush_NilResource(t *testing.T) {
|
||||||
|
a, s := getMockAdapter(t, true, true)
|
||||||
|
defer s.Close()
|
||||||
|
var resources = []*model.Resource{nil}
|
||||||
|
|
||||||
|
err := a.PrepareForPush(resources)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdapter_PrepareForPush_NilMeata(t *testing.T) {
|
||||||
|
a, s := getMockAdapter(t, true, true)
|
||||||
|
defer s.Close()
|
||||||
|
resources := []*model.Resource{
|
||||||
|
{
|
||||||
|
Type: model.ResourceTypeImage,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.PrepareForPush(resources)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdapter_PrepareForPush_NilRepository(t *testing.T) {
|
||||||
|
a, s := getMockAdapter(t, true, true)
|
||||||
|
defer s.Close()
|
||||||
|
resources := []*model.Resource{
|
||||||
|
{
|
||||||
|
Type: model.ResourceTypeImage,
|
||||||
|
Metadata: &model.ResourceMetadata{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.PrepareForPush(resources)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdapter_PrepareForPush_NilRepositoryName(t *testing.T) {
|
||||||
|
a, s := getMockAdapter(t, true, true)
|
||||||
|
defer s.Close()
|
||||||
|
resources := []*model.Resource{
|
||||||
|
{
|
||||||
|
Type: model.ResourceTypeImage,
|
||||||
|
Metadata: &model.ResourceMetadata{
|
||||||
|
Repository: &model.Repository{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.PrepareForPush(resources)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
181
src/replication/adapter/tencentcr/artifact_registry.go
Normal file
181
src/replication/adapter/tencentcr/artifact_registry.go
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
package tencentcr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common/utils"
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
adp "github.com/goharbor/harbor/src/replication/adapter"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
"github.com/goharbor/harbor/src/replication/util"
|
||||||
|
tcr "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tcr/v20190924"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tcrQPSLimit = 15
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement ArtifactRegistry Interface
|
||||||
|
**/
|
||||||
|
var _ adp.ArtifactRegistry = &adapter{}
|
||||||
|
|
||||||
|
func filterToPatterns(filters []*model.Filter) (namespacePattern, repoPattern, tagsPattern string) {
|
||||||
|
for _, filter := range filters {
|
||||||
|
if filter.Type == model.FilterTypeName {
|
||||||
|
repoPattern = filter.Value.(string)
|
||||||
|
}
|
||||||
|
if filter.Type == model.FilterTypeTag {
|
||||||
|
tagsPattern = filter.Value.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
namespacePattern = strings.Split(repoPattern, "/")[0]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) FetchArtifacts(filters []*model.Filter) (resources []*model.Resource, err error) {
|
||||||
|
// get filter pattern
|
||||||
|
var namespacePattern, repoPattern, tagsPattern = filterToPatterns(filters)
|
||||||
|
log.Debugf("[tencent-tcr.FetchArtifacts] namespacePattern=%s repoPattern=%s tagsPattern=%s", namespacePattern, repoPattern, tagsPattern)
|
||||||
|
|
||||||
|
// 1. list namespaces
|
||||||
|
var namespaces []string
|
||||||
|
namespaces, err = a.listCandidateNamespaces(namespacePattern)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debugf("[tencent-tcr.FetchArtifacts] namespaces=%v", namespaces)
|
||||||
|
|
||||||
|
// 2. list repos
|
||||||
|
var filteredRepos []tcr.TcrRepositoryInfo
|
||||||
|
for _, ns := range namespaces {
|
||||||
|
var repos []tcr.TcrRepositoryInfo
|
||||||
|
repos, err = a.listReposByNamespace(ns)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debugf("[tencent-tcr.FetchArtifacts] namespace=%s, repositories=%d", ns, len(repos))
|
||||||
|
|
||||||
|
if _, ok := util.IsSpecificPathComponent(repoPattern); ok {
|
||||||
|
log.Debugf("[tencent-tcr.FetchArtifacts] specific_repos=%s", repoPattern)
|
||||||
|
// TODO: Check repo is exist.
|
||||||
|
filteredRepos = append(filteredRepos, repos...)
|
||||||
|
} else {
|
||||||
|
// 3. filter repos
|
||||||
|
for _, repo := range repos {
|
||||||
|
var ok bool
|
||||||
|
ok, err = util.Match(repoPattern, *repo.Name)
|
||||||
|
log.Debugf("[tencent-tcr.FetchArtifacts] namespace=%s, repository=%s, repoPattern=%s, Match=%v", *repo.Namespace, *repo.Name, repoPattern, ok)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
filteredRepos = append(filteredRepos, repo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("[tencent-tcr.FetchArtifacts] filteredRepos=%d", len(filteredRepos))
|
||||||
|
|
||||||
|
// 4. list images
|
||||||
|
var rawResources = make([]*model.Resource, len(filteredRepos))
|
||||||
|
runner := utils.NewLimitedConcurrentRunner(tcrQPSLimit)
|
||||||
|
|
||||||
|
for i, r := range filteredRepos {
|
||||||
|
// !copy
|
||||||
|
index := i
|
||||||
|
repo := r
|
||||||
|
|
||||||
|
runner.AddTask(func() error {
|
||||||
|
var images []string
|
||||||
|
_, images, err = a.getImages(*repo.Namespace, *repo.Name, "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("[tencent-tcr.FetchArtifacts.listImages] repo=%s, error=%v", *repo.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var filteredImages []string
|
||||||
|
if tagsPattern != "" {
|
||||||
|
for _, image := range images {
|
||||||
|
var ok bool
|
||||||
|
ok, err = util.Match(tagsPattern, image)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("[tencent-tcr.FetchArtifacts.matchImage] image='%s', error=%v", image, err)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
filteredImages = append(filteredImages, image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filteredImages = images
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("[tencent-tcr.FetchArtifacts] repo=%s, images=%v, filteredImages=%v", *repo.Name, images, filteredImages)
|
||||||
|
|
||||||
|
if len(filteredImages) > 0 {
|
||||||
|
rawResources[index] = &model.Resource{
|
||||||
|
Type: model.ResourceTypeImage,
|
||||||
|
Registry: a.registry,
|
||||||
|
Metadata: &model.ResourceMetadata{
|
||||||
|
Repository: &model.Repository{
|
||||||
|
Name: *repo.Name,
|
||||||
|
},
|
||||||
|
Vtags: filteredImages,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err = runner.Wait(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch artifacts: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, res := range rawResources {
|
||||||
|
if res != nil {
|
||||||
|
resources = append(resources, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("[tencent-tcr.FetchArtifacts] resources.size=%d", len(resources))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) listCandidateNamespaces(namespacePattern string) (namespaces []string, err error) {
|
||||||
|
// filter namespaces
|
||||||
|
if len(namespacePattern) > 0 {
|
||||||
|
if nms, ok := util.IsSpecificPathComponent(namespacePattern); ok {
|
||||||
|
// Check is exist
|
||||||
|
var exist bool
|
||||||
|
for _, ns := range nms {
|
||||||
|
exist, err = a.isNamespaceExist(ns)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
namespaces = append(namespaces, nms...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(namespaces) > 0 {
|
||||||
|
log.Debugf("[tencent-tcr.listCandidateNamespaces] pattern=%s, namespaces=%v", namespacePattern, namespaces)
|
||||||
|
return namespaces, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// list all
|
||||||
|
return a.listNamespaces()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) DeleteManifest(repository, reference string) (err error) {
|
||||||
|
parts := strings.Split(repository, "/")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return fmt.Errorf("tcr only support repo in format <namespace>/<name>, but got: %s", repository)
|
||||||
|
}
|
||||||
|
log.Warningf("[tencent-tcr.DeleteManifest] namespace=%s, repository=%s, tag=%s", parts[0], parts[1], reference)
|
||||||
|
|
||||||
|
return a.deleteImage(parts[0], parts[1], reference)
|
||||||
|
}
|
170
src/replication/adapter/tencentcr/artifact_registry_test.go
Normal file
170
src/replication/adapter/tencentcr/artifact_registry_test.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package tencentcr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
commonhttp "github.com/goharbor/harbor/src/common/http"
|
||||||
|
"github.com/goharbor/harbor/src/replication/adapter/native"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
tcr "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tcr/v20190924"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_filterToPatterns(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
filters []*model.Filter
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantNamespacePattern string
|
||||||
|
wantRepoPattern string
|
||||||
|
wantTagsPattern string
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotNamespacePattern, gotRepoPattern, gotTagsPattern := filterToPatterns(tt.args.filters)
|
||||||
|
if gotNamespacePattern != tt.wantNamespacePattern {
|
||||||
|
t.Errorf("filterToPatterns() gotNamespacePattern = %v, want %v", gotNamespacePattern, tt.wantNamespacePattern)
|
||||||
|
}
|
||||||
|
if gotRepoPattern != tt.wantRepoPattern {
|
||||||
|
t.Errorf("filterToPatterns() gotRepoPattern = %v, want %v", gotRepoPattern, tt.wantRepoPattern)
|
||||||
|
}
|
||||||
|
if gotTagsPattern != tt.wantTagsPattern {
|
||||||
|
t.Errorf("filterToPatterns() gotTagsPattern = %v, want %v", gotTagsPattern, tt.wantTagsPattern)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_adapter_FetchArtifacts(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Adapter *native.Adapter
|
||||||
|
registryID *string
|
||||||
|
regionName *string
|
||||||
|
tcrClient *tcr.Client
|
||||||
|
pageSize *int64
|
||||||
|
client *commonhttp.Client
|
||||||
|
registry *model.Registry
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
filters []*model.Filter
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantResources []*model.Resource
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
a := &adapter{
|
||||||
|
Adapter: tt.fields.Adapter,
|
||||||
|
registryID: tt.fields.registryID,
|
||||||
|
regionName: tt.fields.regionName,
|
||||||
|
tcrClient: tt.fields.tcrClient,
|
||||||
|
pageSize: tt.fields.pageSize,
|
||||||
|
client: tt.fields.client,
|
||||||
|
registry: tt.fields.registry,
|
||||||
|
}
|
||||||
|
gotResources, err := a.FetchArtifacts(tt.args.filters)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("adapter.FetchArtifacts() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(gotResources, tt.wantResources) {
|
||||||
|
t.Errorf("adapter.FetchArtifacts() = %v, want %v", gotResources, tt.wantResources)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_adapter_listCandidateNamespaces(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Adapter *native.Adapter
|
||||||
|
registryID *string
|
||||||
|
regionName *string
|
||||||
|
tcrClient *tcr.Client
|
||||||
|
pageSize *int64
|
||||||
|
client *commonhttp.Client
|
||||||
|
registry *model.Registry
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
namespacePattern string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantNamespaces []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
a := &adapter{
|
||||||
|
Adapter: tt.fields.Adapter,
|
||||||
|
registryID: tt.fields.registryID,
|
||||||
|
regionName: tt.fields.regionName,
|
||||||
|
tcrClient: tt.fields.tcrClient,
|
||||||
|
pageSize: tt.fields.pageSize,
|
||||||
|
client: tt.fields.client,
|
||||||
|
registry: tt.fields.registry,
|
||||||
|
}
|
||||||
|
gotNamespaces, err := a.listCandidateNamespaces(tt.args.namespacePattern)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("adapter.listCandidateNamespaces() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(gotNamespaces, tt.wantNamespaces) {
|
||||||
|
t.Errorf("adapter.listCandidateNamespaces() = %v, want %v", gotNamespaces, tt.wantNamespaces)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_adapter_DeleteManifest(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Adapter *native.Adapter
|
||||||
|
registryID *string
|
||||||
|
regionName *string
|
||||||
|
tcrClient *tcr.Client
|
||||||
|
pageSize *int64
|
||||||
|
client *commonhttp.Client
|
||||||
|
registry *model.Registry
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
repository string
|
||||||
|
reference string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
a := &adapter{
|
||||||
|
Adapter: tt.fields.Adapter,
|
||||||
|
registryID: tt.fields.registryID,
|
||||||
|
regionName: tt.fields.regionName,
|
||||||
|
tcrClient: tt.fields.tcrClient,
|
||||||
|
pageSize: tt.fields.pageSize,
|
||||||
|
client: tt.fields.client,
|
||||||
|
registry: tt.fields.registry,
|
||||||
|
}
|
||||||
|
if err := a.DeleteManifest(tt.args.repository, tt.args.reference); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("adapter.DeleteManifest() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
85
src/replication/adapter/tencentcr/auth.go
Normal file
85
src/replication/adapter/tencentcr/auth.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package tencentcr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common/http/modifier"
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
tcr "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tcr/v20190924"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Credential ...
|
||||||
|
type Credential modifier.Modifier
|
||||||
|
|
||||||
|
var _ Credential = &qcloudAuthCredential{}
|
||||||
|
|
||||||
|
func (q *qcloudAuthCredential) Modify(r *http.Request) (err error) {
|
||||||
|
if !q.isCacheTokenValid() {
|
||||||
|
err = q.getTempInstanceToken()
|
||||||
|
log.Debugf("qcloudAuthCredential.Modify.isCacheTokenValid.updateToken=%s, err=%v", q.cacheTokenExpiredAt, err)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.SetBasicAuth(q.cacheTokener.username, q.cacheTokener.token)
|
||||||
|
log.Debugf("[qcloudAuthCredential.Modify]Host: %v, header: %#v", r.Host, r.Header)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *qcloudAuthCredential) isCacheTokenValid() (ok bool) {
|
||||||
|
if &q.cacheTokenExpiredAt == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if q.cacheTokener == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if time.Now().After(q.cacheTokenExpiredAt) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements interface Credential
|
||||||
|
type qcloudAuthCredential struct {
|
||||||
|
registryID *string
|
||||||
|
client *tcr.Client
|
||||||
|
cacheTokener *temporaryTokener
|
||||||
|
cacheTokenExpiredAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type temporaryTokener struct {
|
||||||
|
username string
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuth ...
|
||||||
|
func NewAuth(registryID *string, client *tcr.Client) Credential {
|
||||||
|
return &qcloudAuthCredential{
|
||||||
|
registryID: registryID,
|
||||||
|
client: client,
|
||||||
|
cacheTokener: &temporaryTokener{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *qcloudAuthCredential) getTempInstanceToken() (err error) {
|
||||||
|
var req = tcr.NewCreateInstanceTokenRequest()
|
||||||
|
req.RegistryId = q.registryID
|
||||||
|
var resp *tcr.CreateInstanceTokenResponse
|
||||||
|
resp, err = q.client.CreateInstanceToken(req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
q.cacheTokener = &temporaryTokener{*resp.Response.Username, *resp.Response.Token}
|
||||||
|
q.cacheTokenExpiredAt = time.Unix(*resp.Response.ExpTime/1e3, *resp.Response.ExpTime%1e3)
|
||||||
|
log.Debugf("[qcloudAuthCredential.getTempInstanceToken]Update temp token=%#v, cacheTokenExpiredAt=%s, unix=%v", q.cacheTokener,
|
||||||
|
q.cacheTokenExpiredAt.UTC().String(), *resp.Response.ExpTime)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSecretID(key string) (ok bool) {
|
||||||
|
return strings.Index(key, "AKID") == 0
|
||||||
|
}
|
267
src/replication/adapter/tencentcr/chart_registry.go
Normal file
267
src/replication/adapter/tencentcr/chart_registry.go
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
package tencentcr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
commonhttp "github.com/goharbor/harbor/src/common/http"
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
adp "github.com/goharbor/harbor/src/replication/adapter"
|
||||||
|
"github.com/goharbor/harbor/src/replication/filter"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
chartListURL = "%s/api/chartrepo/%s/charts"
|
||||||
|
chartVersionURL = "%s/api/chartrepo/%s/charts/%s"
|
||||||
|
chartInfoURL = "%s/api/chartrepo/%s/charts/%s/%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tcrChart struct {
|
||||||
|
APIVersion string `json:"apiVersion"`
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
URLs []string `json:"urls"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tcrChartVersionDetail struct {
|
||||||
|
Metadata *tcrChartVersionMetadata `json:"metadata"`
|
||||||
|
}
|
||||||
|
type tcrChartVersionMetadata struct {
|
||||||
|
URLs []string `json:"urls"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adp.ChartRegistry = &adapter{}
|
||||||
|
|
||||||
|
func (a *adapter) FetchCharts(filters []*model.Filter) (resources []*model.Resource, err error) {
|
||||||
|
log.Debugf("[tencent-tcr.FetchCharts]filters: %#v", filters)
|
||||||
|
// 1. list namespaces
|
||||||
|
var nsPattern, _, _ = filterToPatterns(filters)
|
||||||
|
var nms []string
|
||||||
|
nms, err = a.listCandidateNamespaces(nsPattern)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. list repositories
|
||||||
|
for _, ns := range nms {
|
||||||
|
var url = fmt.Sprintf(chartListURL, a.registry.URL, ns)
|
||||||
|
var repositories = []*model.Repository{}
|
||||||
|
err = a.client.Get(url, &repositories)
|
||||||
|
log.Debugf("[tencent-tcr.FetchCharts] url=%s, namespace=%s, repositories=%v, error=%v", url, ns, repositories, err)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(repositories) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, repository := range repositories {
|
||||||
|
repository.Name = fmt.Sprintf("%s/%s", ns, repository.Name)
|
||||||
|
}
|
||||||
|
repositories, err = filter.DoFilterRepositories(repositories, filters)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. list versions
|
||||||
|
for _, repository := range repositories {
|
||||||
|
var name = strings.SplitN(repository.Name, "/", 2)[1]
|
||||||
|
var url = fmt.Sprintf(chartVersionURL, a.registry.URL, ns, name)
|
||||||
|
var charts = []*tcrChart{}
|
||||||
|
err = a.client.Get(url, &charts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(charts) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var artifacts []*model.Artifact
|
||||||
|
for _, chart := range charts {
|
||||||
|
artifacts = append(artifacts, &model.Artifact{
|
||||||
|
Tags: []string{chart.Version},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
artifacts, err = filter.DoFilterArtifacts(artifacts, filters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(artifacts) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, artifact := range artifacts {
|
||||||
|
resources = append(resources, &model.Resource{
|
||||||
|
Type: model.ResourceTypeChart,
|
||||||
|
Registry: a.registry,
|
||||||
|
Metadata: &model.ResourceMetadata{
|
||||||
|
Repository: &model.Repository{
|
||||||
|
Name: repository.Name,
|
||||||
|
},
|
||||||
|
Artifacts: []*model.Artifact{artifact},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) ChartExist(name, version string) (exist bool, err error) {
|
||||||
|
log.Debugf("[tencent-tcr.ChartExist] name=%s version=%s", name, version)
|
||||||
|
_, err = a.getChartInfo(name, version)
|
||||||
|
// if not found, return not exist
|
||||||
|
if httpErr, ok := err.(*commonhttp.Error); ok && httpErr.Code == http.StatusNotFound {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exist = true
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) getChartInfo(name, version string) (info *tcrChartVersionDetail, err error) {
|
||||||
|
var namespace string
|
||||||
|
var chart string
|
||||||
|
namespace, chart, err = parseChartName(name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = fmt.Sprintf(chartInfoURL, a.registry.URL, namespace, chart, version)
|
||||||
|
info = &tcrChartVersionDetail{}
|
||||||
|
err = a.client.Get(url, info)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) DownloadChart(name, version string) (rc io.ReadCloser, err error) {
|
||||||
|
var info *tcrChartVersionDetail
|
||||||
|
info, err = a.getChartInfo(name, version)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if info.Metadata == nil || len(info.Metadata.URLs) == 0 || len(info.Metadata.URLs[0]) == 0 {
|
||||||
|
return nil, fmt.Errorf("[tencent-tcr.DownloadChart.NO_DOWNLOAD_URL] chart=%s:%s", name, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = strings.ToLower(info.Metadata.URLs[0])
|
||||||
|
// relative URL
|
||||||
|
if !(strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")) {
|
||||||
|
var namespace string
|
||||||
|
namespace, _, err = parseChartName(name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
url = fmt.Sprintf("%s/chartrepo/%s/%s", a.registry.URL, namespace, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
var req *http.Request
|
||||||
|
var resp *http.Response
|
||||||
|
var body []byte
|
||||||
|
req, err = http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err = a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, err = ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("[tencent-tcr.DownloadChart.failed] chart=%s, status=%d, body=%s", req.URL.String(), resp.StatusCode, string(body))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) UploadChart(name, version string, reader io.Reader) (err error) {
|
||||||
|
var namespace string
|
||||||
|
var chart string
|
||||||
|
namespace, chart, err = parseChartName(name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. write to form-data buffer
|
||||||
|
var buf = &bytes.Buffer{}
|
||||||
|
var writer = multipart.NewWriter(buf)
|
||||||
|
var fw io.Writer
|
||||||
|
fw, err = writer.CreateFormFile("chart", chart+".tgz")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = io.Copy(fw, reader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
// 2. upload
|
||||||
|
var url = fmt.Sprintf(chartListURL, a.registry.URL, namespace)
|
||||||
|
var req *http.Request
|
||||||
|
var resp *http.Response
|
||||||
|
req, err = http.NewRequest(http.MethodPost, url, buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
resp, err = a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 3. parse response
|
||||||
|
var data []byte
|
||||||
|
data, err = ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode < http.StatusOK || resp.StatusCode > 299 {
|
||||||
|
err = &commonhttp.Error{
|
||||||
|
Code: resp.StatusCode,
|
||||||
|
Message: string(data),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) DeleteChart(name, version string) (err error) {
|
||||||
|
var namespace string
|
||||||
|
var chart string
|
||||||
|
namespace, chart, err = parseChartName(name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = fmt.Sprintf(chartInfoURL, a.registry.URL, namespace, chart, version)
|
||||||
|
|
||||||
|
return a.client.Delete(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseChartName(name string) (namespace, chart string, err error) {
|
||||||
|
strs := strings.Split(name, "/")
|
||||||
|
if len(strs) == 2 && len(strs[0]) > 0 && len(strs[1]) > 0 {
|
||||||
|
return strs[0], strs[1], nil
|
||||||
|
}
|
||||||
|
return "", "", fmt.Errorf("[tencent-tcr.parseChartName.invalid_name] name=%s", name)
|
||||||
|
}
|
240
src/replication/adapter/tencentcr/sdk.go
Normal file
240
src/replication/adapter/tencentcr/sdk.go
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
package tencentcr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
||||||
|
tcr "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tcr/v20190924"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *adapter) createPrivateNamespace(namespace string) (err error) {
|
||||||
|
if a.tcrClient == nil {
|
||||||
|
err = errors.New("[tencent-tcr.createPrivateNamespace] nil tcr client")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. if exist skip
|
||||||
|
log.Debugf("[tencent-tcr.PrepareForPush.createPrivateNamespace] namespace=%s", namespace)
|
||||||
|
var exist bool
|
||||||
|
exist, err = a.isNamespaceExist(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if exist {
|
||||||
|
log.Warningf("[tencent-tcr.PrepareForPush.createPrivateNamespace.skip_exist] namespace=%s", namespace)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// !!! 2. WARNING: for safety, auto create namespace is private.
|
||||||
|
var req = tcr.NewCreateNamespaceRequest()
|
||||||
|
req.NamespaceName = &namespace
|
||||||
|
req.RegistryId = a.registryID
|
||||||
|
var isPublic = false
|
||||||
|
req.IsPublic = &isPublic
|
||||||
|
tcr.NewCreateNamespaceResponse()
|
||||||
|
_, err = a.tcrClient.CreateNamespace(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("[tencent-tcr.PrepareForPush.createPrivateNamespace] error=%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) createRepository(namespace, repository string) (err error) {
|
||||||
|
if a.tcrClient == nil {
|
||||||
|
err = errors.New("[tencent-tcr.createRepository] nil tcr client")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. if exist skip
|
||||||
|
log.Debugf("[tencent-tcr.PrepareForPush.createRepository] namespace=%s, repository=%s", namespace, repository)
|
||||||
|
var repoReq = tcr.NewDescribeRepositoriesRequest()
|
||||||
|
repoReq.RegistryId = a.registryID
|
||||||
|
repoReq.NamespaceName = &namespace
|
||||||
|
repoReq.RepositoryName = &repository
|
||||||
|
var repoResp = tcr.NewDescribeRepositoriesResponse()
|
||||||
|
repoResp, err = a.tcrClient.DescribeRepositories(repoReq)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if int(*repoResp.Response.TotalCount) > 0 {
|
||||||
|
log.Warningf("[tencent-tcr.PrepareForPush.createRepository.skip_exist] namespace=%s, repository=%s", namespace, repository)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. create
|
||||||
|
var req = tcr.NewCreateRepositoryRequest()
|
||||||
|
req.NamespaceName = &namespace
|
||||||
|
req.RepositoryName = &repository
|
||||||
|
req.RegistryId = a.registryID
|
||||||
|
var resp = tcr.NewCreateRepositoryResponse()
|
||||||
|
resp, err = a.tcrClient.CreateRepository(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("[tencent-tcr.PrepareForPush.createRepository] error=%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debugf("[tencent-tcr.PrepareForPush.createRepository] resp=%#v", *resp)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) listNamespaces() (namespaces []string, err error) {
|
||||||
|
if a.tcrClient == nil {
|
||||||
|
err = errors.New("[tencent-tcr.listNamespaces] nil tcr client")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// list namespaces
|
||||||
|
var req = tcr.NewDescribeNamespacesRequest()
|
||||||
|
req.RegistryId = a.registryID
|
||||||
|
req.Limit = a.pageSize
|
||||||
|
var resp = tcr.NewDescribeNamespacesResponse()
|
||||||
|
|
||||||
|
var page int64
|
||||||
|
for {
|
||||||
|
req.Offset = &page
|
||||||
|
resp, err = a.tcrClient.DescribeNamespaces(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("[tencent-tcr.DescribeNamespaces] registryID=%s, error=%v", *a.registryID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ns := range resp.Response.NamespaceList {
|
||||||
|
namespaces = append(namespaces, *ns.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(namespaces) >= int(*resp.Response.TotalCount) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("[tencent-tcr.FetchArtifacts.listNamespaces] registryID=%s, namespaces[%d]=%s", *a.registryID, len(namespaces), namespaces)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) isNamespaceExist(namespace string) (exist bool, err error) {
|
||||||
|
if a.tcrClient == nil {
|
||||||
|
err = errors.New("[tencent-tcr.isNamespaceExist] nil tcr client")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req = tcr.NewDescribeNamespacesRequest()
|
||||||
|
req.NamespaceName = &namespace
|
||||||
|
req.RegistryId = a.registryID
|
||||||
|
var resp = tcr.NewDescribeNamespacesResponse()
|
||||||
|
resp, err = a.tcrClient.DescribeNamespaces(req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warningf("[tencent-tcr.PrepareForPush.isNamespaceExist] namespace=%s, total=%d", namespace, *resp.Response.TotalCount)
|
||||||
|
if int(*resp.Response.TotalCount) != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exist = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) listReposByNamespace(namespace string) (repos []tcr.TcrRepositoryInfo, err error) {
|
||||||
|
if a.tcrClient == nil {
|
||||||
|
err = errors.New("[tencent-tcr.listReposByNamespace] nil tcr client")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req = tcr.NewDescribeRepositoriesRequest()
|
||||||
|
req.RegistryId = a.registryID
|
||||||
|
req.NamespaceName = common.StringPtr(namespace)
|
||||||
|
req.Limit = a.pageSize
|
||||||
|
var resp = tcr.NewDescribeRepositoriesResponse()
|
||||||
|
|
||||||
|
var page int64
|
||||||
|
for {
|
||||||
|
req.Offset = common.Int64Ptr(page)
|
||||||
|
resp, err = a.tcrClient.DescribeRepositories(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("[tencent-tcr.listReposByNamespace.DescribeRepositories] registryID=%s, namespace=%s, error=%v", *a.registryID, namespace, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
size := len(resp.Response.RepositoryList)
|
||||||
|
for i, repo := range resp.Response.RepositoryList {
|
||||||
|
log.Debugf("[tencent-tcr.listReposByNamespace.DescribeRepositories] Retrives page=%d repo(%d/%d)=%s", page, i, size, *repo.Name)
|
||||||
|
repos = append(repos, *repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(repos) == int(*resp.Response.TotalCount) {
|
||||||
|
log.Debugf("[tencent-tcr.listReposByNamespace.DescribeRepositories] Retrives all repos.")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("[tencent-tcr.listReposByNamespace] registryID=%s, namespace=%s, repos=%d",
|
||||||
|
*a.registryID, namespace, len(repos))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) getImages(namespace, repo, tag string) (images []*tcr.TcrImageInfo, imageNames []string, err error) {
|
||||||
|
if a.tcrClient == nil {
|
||||||
|
err = errors.New("[tencent-tcr.getImages] nil tcr client")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if namespace != "" {
|
||||||
|
repo = strings.Replace(repo, namespace, "", 1)
|
||||||
|
repo = strings.Replace(repo, "/", "", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var req = tcr.NewDescribeImagesRequest()
|
||||||
|
req.RegistryId = a.registryID
|
||||||
|
req.NamespaceName = &namespace
|
||||||
|
req.RepositoryName = &repo
|
||||||
|
req.Limit = a.pageSize
|
||||||
|
if tag != "" {
|
||||||
|
req.ImageVersion = &tag
|
||||||
|
}
|
||||||
|
var resp = tcr.NewDescribeImagesResponse()
|
||||||
|
|
||||||
|
var page int64
|
||||||
|
for {
|
||||||
|
log.Debugf("[tencent-tcr.getImages] registryID=%s, namespace=%s, repo=%s, tag=%s, page=%d",
|
||||||
|
*a.registryID, namespace, repo, tag, page)
|
||||||
|
req.Offset = &page
|
||||||
|
resp, err = a.tcrClient.DescribeImages(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("[tencent-tcr.getImages.DescribeImages] registryID=%s, namespace=%s, repo=%s, error=%v", *a.registryID, namespace, repo, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
images = resp.Response.ImageInfoList
|
||||||
|
for _, image := range resp.Response.ImageInfoList {
|
||||||
|
imageNames = append(imageNames, *image.ImageVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(images) == int(*resp.Response.TotalCount) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("[tencent-tcr.getImages] registryID=%s, namespace=%s, repo=%s, tags[%d]=%v\n", *a.registryID, namespace, repo, len(imageNames), imageNames)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapter) deleteImage(namespace, repository, reference string) (err error) {
|
||||||
|
var req = tcr.NewDeleteImageRequest()
|
||||||
|
req.RegistryId = a.registryID
|
||||||
|
req.NamespaceName = common.StringPtr(namespace)
|
||||||
|
req.RepositoryName = common.StringPtr(repository)
|
||||||
|
req.ImageVersion = common.StringPtr(reference)
|
||||||
|
|
||||||
|
_, err = a.tcrClient.DeleteImage(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("[tencent-tcr.deleteImage.DeleteImage] failed. namespace=%s, repository=%s, tag=%s, error=%s", namespace, repository, reference, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
133
src/replication/adapter/tencentcr/sdk_test.go
Normal file
133
src/replication/adapter/tencentcr/sdk_test.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package tencentcr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_adapter_createPrivateNamespace(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
namespace string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{namespace: "ut_ns_123", wantErr: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
a := &adapter{tcrClient: tcrClient}
|
||||||
|
if err := a.createPrivateNamespace(tt.namespace); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("adapter.createPrivateNamespace() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_adapter_createRepository(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
namespace string
|
||||||
|
repository string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{namespace: "ut_ns_123", repository: "ut_repo_123", wantErr: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
a := &adapter{tcrClient: tcrClient}
|
||||||
|
if err := a.createRepository(tt.namespace, tt.repository); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("adapter.createRepository() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_adapter_listNamespaces(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
wantNamespaces []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{wantNamespaces: []string{}, wantErr: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
a := &adapter{tcrClient: tcrClient}
|
||||||
|
_, err := a.listNamespaces()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("adapter.listNamespaces() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_adapter_isNamespaceExist(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
namespace string
|
||||||
|
wantExist bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{namespace: "ut_ns_123", wantErr: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
a := &adapter{tcrClient: tcrClient}
|
||||||
|
gotExist, err := a.isNamespaceExist(tt.namespace)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("adapter.isNamespaceExist() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if gotExist != tt.wantExist {
|
||||||
|
t.Errorf("adapter.isNamespaceExist() = %v, want %v", gotExist, tt.wantExist)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_adapter_listReposByNamespace(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
namespace string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{namespace: "ut_ns_123", wantErr: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
a := &adapter{tcrClient: tcrClient}
|
||||||
|
_, err := a.listReposByNamespace(tt.namespace)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("adapter.listReposByNamespace() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_adapter_getImages(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
namespace string
|
||||||
|
repo string
|
||||||
|
tag string
|
||||||
|
wantImages []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{namespace: "ut_ns_123", repo: "ut_repo_123", wantErr: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
a := &adapter{tcrClient: tcrClient}
|
||||||
|
_, gotImages, err := a.getImages(tt.namespace, tt.repo, tt.tag)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("adapter.getImages() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(gotImages, tt.wantImages) {
|
||||||
|
t.Errorf("adapter.getImages() = %v, want %v", gotImages, tt.wantImages)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -32,6 +32,7 @@ const (
|
|||||||
RegistryTypeQuay RegistryType = "quay"
|
RegistryTypeQuay RegistryType = "quay"
|
||||||
RegistryTypeGitLab RegistryType = "gitlab"
|
RegistryTypeGitLab RegistryType = "gitlab"
|
||||||
RegistryTypeDTR RegistryType = "dtr"
|
RegistryTypeDTR RegistryType = "dtr"
|
||||||
|
RegistryTypeTencentTcr RegistryType = "tencent-tcr"
|
||||||
|
|
||||||
RegistryTypeHelmHub RegistryType = "helm-hub"
|
RegistryTypeHelmHub RegistryType = "helm-hub"
|
||||||
RegistryTypeArtifactHub RegistryType = "artifact-hub"
|
RegistryTypeArtifactHub RegistryType = "artifact-hub"
|
||||||
|
@ -59,6 +59,8 @@ import (
|
|||||||
_ "github.com/goharbor/harbor/src/replication/adapter/dtr"
|
_ "github.com/goharbor/harbor/src/replication/adapter/dtr"
|
||||||
// register the Artifact Hub adapter
|
// register the Artifact Hub adapter
|
||||||
_ "github.com/goharbor/harbor/src/replication/adapter/artifacthub"
|
_ "github.com/goharbor/harbor/src/replication/adapter/artifacthub"
|
||||||
|
// register the TencentCloud TCR adapter
|
||||||
|
_ "github.com/goharbor/harbor/src/replication/adapter/tencentcr"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
201
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/LICENSE
generated
vendored
Normal file
201
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/LICENSE
generated
vendored
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright (c) 2017-2018 Tencent Ltd.
|
||||||
|
|
||||||
|
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.
|
278
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/client.go
generated
vendored
Normal file
278
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/client.go
generated
vendored
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
|
||||||
|
tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http"
|
||||||
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
region string
|
||||||
|
httpClient *http.Client
|
||||||
|
httpProfile *profile.HttpProfile
|
||||||
|
profile *profile.ClientProfile
|
||||||
|
credential *Credential
|
||||||
|
signMethod string
|
||||||
|
unsignedPayload bool
|
||||||
|
debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Send(request tchttp.Request, response tchttp.Response) (err error) {
|
||||||
|
if request.GetScheme() == "" {
|
||||||
|
request.SetScheme(c.httpProfile.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.GetRootDomain() == "" {
|
||||||
|
request.SetRootDomain(c.httpProfile.RootDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.GetDomain() == "" {
|
||||||
|
domain := c.httpProfile.Endpoint
|
||||||
|
if domain == "" {
|
||||||
|
domain = request.GetServiceDomain(request.GetService())
|
||||||
|
}
|
||||||
|
request.SetDomain(domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.GetHttpMethod() == "" {
|
||||||
|
request.SetHttpMethod(c.httpProfile.ReqMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
tchttp.CompleteCommonParams(request, c.GetRegion())
|
||||||
|
|
||||||
|
if c.signMethod == "HmacSHA1" || c.signMethod == "HmacSHA256" {
|
||||||
|
return c.sendWithSignatureV1(request, response)
|
||||||
|
} else {
|
||||||
|
return c.sendWithSignatureV3(request, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) sendWithSignatureV1(request tchttp.Request, response tchttp.Response) (err error) {
|
||||||
|
// TODO: not an elegant way, it should be done in common params, but finally it need to refactor
|
||||||
|
request.GetParams()["Language"] = c.profile.Language
|
||||||
|
err = tchttp.ConstructParams(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = signRequest(request, c.credential, c.signMethod)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
httpRequest, err := http.NewRequest(request.GetHttpMethod(), request.GetUrl(), request.GetBodyReader())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if request.GetHttpMethod() == "POST" {
|
||||||
|
httpRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
}
|
||||||
|
if c.debug {
|
||||||
|
outbytes, err := httputil.DumpRequest(httpRequest, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] dump request failed because %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] http request = %s", outbytes)
|
||||||
|
}
|
||||||
|
httpResponse, err := c.httpClient.Do(httpRequest)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Fail to get response because %s", err)
|
||||||
|
return errors.NewTencentCloudSDKError("ClientError.NetworkError", msg, "")
|
||||||
|
}
|
||||||
|
err = tchttp.ParseFromHttpResponse(httpResponse, response)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) sendWithSignatureV3(request tchttp.Request, response tchttp.Response) (err error) {
|
||||||
|
headers := map[string]string{
|
||||||
|
"Host": request.GetDomain(),
|
||||||
|
"X-TC-Action": request.GetAction(),
|
||||||
|
"X-TC-Version": request.GetVersion(),
|
||||||
|
"X-TC-Timestamp": request.GetParams()["Timestamp"],
|
||||||
|
"X-TC-RequestClient": request.GetParams()["RequestClient"],
|
||||||
|
"X-TC-Language": c.profile.Language,
|
||||||
|
}
|
||||||
|
if c.region != "" {
|
||||||
|
headers["X-TC-Region"] = c.region
|
||||||
|
}
|
||||||
|
if c.credential.Token != "" {
|
||||||
|
headers["X-TC-Token"] = c.credential.Token
|
||||||
|
}
|
||||||
|
if request.GetHttpMethod() == "GET" {
|
||||||
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
|
} else {
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
// start signature v3 process
|
||||||
|
|
||||||
|
// build canonical request string
|
||||||
|
httpRequestMethod := request.GetHttpMethod()
|
||||||
|
canonicalURI := "/"
|
||||||
|
canonicalQueryString := ""
|
||||||
|
if httpRequestMethod == "GET" {
|
||||||
|
err = tchttp.ConstructParams(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
params := make(map[string]string)
|
||||||
|
for key, value := range request.GetParams() {
|
||||||
|
params[key] = value
|
||||||
|
}
|
||||||
|
delete(params, "Action")
|
||||||
|
delete(params, "Version")
|
||||||
|
delete(params, "Nonce")
|
||||||
|
delete(params, "Region")
|
||||||
|
delete(params, "RequestClient")
|
||||||
|
delete(params, "Timestamp")
|
||||||
|
canonicalQueryString = tchttp.GetUrlQueriesEncoded(params)
|
||||||
|
}
|
||||||
|
canonicalHeaders := fmt.Sprintf("content-type:%s\nhost:%s\n", headers["Content-Type"], headers["Host"])
|
||||||
|
signedHeaders := "content-type;host"
|
||||||
|
requestPayload := ""
|
||||||
|
if httpRequestMethod == "POST" {
|
||||||
|
b, err := json.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
requestPayload = string(b)
|
||||||
|
}
|
||||||
|
hashedRequestPayload := ""
|
||||||
|
if c.unsignedPayload {
|
||||||
|
hashedRequestPayload = sha256hex("UNSIGNED-PAYLOAD")
|
||||||
|
headers["X-TC-Content-SHA256"] = "UNSIGNED-PAYLOAD"
|
||||||
|
} else {
|
||||||
|
hashedRequestPayload = sha256hex(requestPayload)
|
||||||
|
}
|
||||||
|
canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
|
||||||
|
httpRequestMethod,
|
||||||
|
canonicalURI,
|
||||||
|
canonicalQueryString,
|
||||||
|
canonicalHeaders,
|
||||||
|
signedHeaders,
|
||||||
|
hashedRequestPayload)
|
||||||
|
//log.Println("canonicalRequest:", canonicalRequest)
|
||||||
|
|
||||||
|
// build string to sign
|
||||||
|
algorithm := "TC3-HMAC-SHA256"
|
||||||
|
requestTimestamp := headers["X-TC-Timestamp"]
|
||||||
|
timestamp, _ := strconv.ParseInt(requestTimestamp, 10, 64)
|
||||||
|
t := time.Unix(timestamp, 0).UTC()
|
||||||
|
// must be the format 2006-01-02, ref to package time for more info
|
||||||
|
date := t.Format("2006-01-02")
|
||||||
|
credentialScope := fmt.Sprintf("%s/%s/tc3_request", date, request.GetService())
|
||||||
|
hashedCanonicalRequest := sha256hex(canonicalRequest)
|
||||||
|
string2sign := fmt.Sprintf("%s\n%s\n%s\n%s",
|
||||||
|
algorithm,
|
||||||
|
requestTimestamp,
|
||||||
|
credentialScope,
|
||||||
|
hashedCanonicalRequest)
|
||||||
|
//log.Println("string2sign", string2sign)
|
||||||
|
|
||||||
|
// sign string
|
||||||
|
secretDate := hmacsha256(date, "TC3"+c.credential.SecretKey)
|
||||||
|
secretService := hmacsha256(request.GetService(), secretDate)
|
||||||
|
secretKey := hmacsha256("tc3_request", secretService)
|
||||||
|
signature := hex.EncodeToString([]byte(hmacsha256(string2sign, secretKey)))
|
||||||
|
//log.Println("signature", signature)
|
||||||
|
|
||||||
|
// build authorization
|
||||||
|
authorization := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
|
||||||
|
algorithm,
|
||||||
|
c.credential.SecretId,
|
||||||
|
credentialScope,
|
||||||
|
signedHeaders,
|
||||||
|
signature)
|
||||||
|
//log.Println("authorization", authorization)
|
||||||
|
|
||||||
|
headers["Authorization"] = authorization
|
||||||
|
url := request.GetScheme() + "://" + request.GetDomain() + request.GetPath()
|
||||||
|
if canonicalQueryString != "" {
|
||||||
|
url = url + "?" + canonicalQueryString
|
||||||
|
}
|
||||||
|
httpRequest, err := http.NewRequest(httpRequestMethod, url, strings.NewReader(requestPayload))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for k, v := range headers {
|
||||||
|
httpRequest.Header[k] = []string{v}
|
||||||
|
}
|
||||||
|
if c.debug {
|
||||||
|
outbytes, err := httputil.DumpRequest(httpRequest, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] dump request failed because %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] http request = %s", outbytes)
|
||||||
|
}
|
||||||
|
httpResponse, err := c.httpClient.Do(httpRequest)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Fail to get response because %s", err)
|
||||||
|
return errors.NewTencentCloudSDKError("ClientError.NetworkError", msg, "")
|
||||||
|
}
|
||||||
|
err = tchttp.ParseFromHttpResponse(httpResponse, response)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetRegion() string {
|
||||||
|
return c.region
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Init(region string) *Client {
|
||||||
|
c.httpClient = &http.Client{}
|
||||||
|
c.region = region
|
||||||
|
c.signMethod = "TC3-HMAC-SHA256"
|
||||||
|
c.debug = false
|
||||||
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) WithSecretId(secretId, secretKey string) *Client {
|
||||||
|
c.credential = NewCredential(secretId, secretKey)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) WithCredential(cred *Credential) *Client {
|
||||||
|
c.credential = cred
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) WithProfile(clientProfile *profile.ClientProfile) *Client {
|
||||||
|
c.profile = clientProfile
|
||||||
|
c.signMethod = clientProfile.SignMethod
|
||||||
|
c.unsignedPayload = clientProfile.UnsignedPayload
|
||||||
|
c.httpProfile = clientProfile.HttpProfile
|
||||||
|
c.debug = clientProfile.Debug
|
||||||
|
c.httpClient.Timeout = time.Duration(c.httpProfile.ReqTimeout) * time.Second
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) WithSignatureMethod(method string) *Client {
|
||||||
|
c.signMethod = method
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) WithHttpTransport(transport http.RoundTripper) *Client {
|
||||||
|
c.httpClient.Transport = transport
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) WithDebug(flag bool) *Client {
|
||||||
|
c.debug = flag
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClientWithSecretId(secretId, secretKey, region string) (client *Client, err error) {
|
||||||
|
client = &Client{}
|
||||||
|
client.Init(region).WithSecretId(secretId, secretKey)
|
||||||
|
return
|
||||||
|
}
|
58
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/credentials.go
generated
vendored
Normal file
58
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/credentials.go
generated
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
type Credential struct {
|
||||||
|
SecretId string
|
||||||
|
SecretKey string
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCredential(secretId, secretKey string) *Credential {
|
||||||
|
return &Credential{
|
||||||
|
SecretId: secretId,
|
||||||
|
SecretKey: secretKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTokenCredential(secretId, secretKey, token string) *Credential {
|
||||||
|
return &Credential{
|
||||||
|
SecretId: secretId,
|
||||||
|
SecretKey: secretKey,
|
||||||
|
Token: token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Credential) GetCredentialParams() map[string]string {
|
||||||
|
p := map[string]string{
|
||||||
|
"SecretId": c.SecretId,
|
||||||
|
}
|
||||||
|
if c.Token != "" {
|
||||||
|
p["Token"] = c.Token
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nowhere use them and we haven't well designed these structures and
|
||||||
|
// underlying method, which leads to the situation that it is hard to
|
||||||
|
// refactor it to interfaces.
|
||||||
|
// Hence they are removed and merged into Credential.
|
||||||
|
|
||||||
|
//type TokenCredential struct {
|
||||||
|
// SecretId string
|
||||||
|
// SecretKey string
|
||||||
|
// Token string
|
||||||
|
//}
|
||||||
|
|
||||||
|
//func NewTokenCredential(secretId, secretKey, token string) *TokenCredential {
|
||||||
|
// return &TokenCredential{
|
||||||
|
// SecretId: secretId,
|
||||||
|
// SecretKey: secretKey,
|
||||||
|
// Token: token,
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
//func (c *TokenCredential) GetCredentialParams() map[string]string {
|
||||||
|
// return map[string]string{
|
||||||
|
// "SecretId": c.SecretId,
|
||||||
|
// "Token": c.Token,
|
||||||
|
// }
|
||||||
|
//}
|
35
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors/errors.go
generated
vendored
Normal file
35
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors/errors.go
generated
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TencentCloudSDKError struct {
|
||||||
|
Code string
|
||||||
|
Message string
|
||||||
|
RequestId string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *TencentCloudSDKError) Error() string {
|
||||||
|
return fmt.Sprintf("[TencentCloudSDKError] Code=%s, Message=%s, RequestId=%s", e.Code, e.Message, e.RequestId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTencentCloudSDKError(code, message, requestId string) error {
|
||||||
|
return &TencentCloudSDKError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
RequestId: requestId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *TencentCloudSDKError) GetCode() string {
|
||||||
|
return e.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *TencentCloudSDKError) GetMessage() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *TencentCloudSDKError) GetRequestId() string {
|
||||||
|
return e.RequestId
|
||||||
|
}
|
275
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/request.go
generated
vendored
Normal file
275
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/request.go
generated
vendored
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
//"log"
|
||||||
|
"math/rand"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
POST = "POST"
|
||||||
|
GET = "GET"
|
||||||
|
|
||||||
|
HTTP = "http"
|
||||||
|
HTTPS = "https"
|
||||||
|
|
||||||
|
RootDomain = "tencentcloudapi.com"
|
||||||
|
Path = "/"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request interface {
|
||||||
|
GetAction() string
|
||||||
|
GetBodyReader() io.Reader
|
||||||
|
GetScheme() string
|
||||||
|
GetRootDomain() string
|
||||||
|
GetServiceDomain(string) string
|
||||||
|
GetDomain() string
|
||||||
|
GetHttpMethod() string
|
||||||
|
GetParams() map[string]string
|
||||||
|
GetPath() string
|
||||||
|
GetService() string
|
||||||
|
GetUrl() string
|
||||||
|
GetVersion() string
|
||||||
|
SetScheme(string)
|
||||||
|
SetRootDomain(string)
|
||||||
|
SetDomain(string)
|
||||||
|
SetHttpMethod(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseRequest struct {
|
||||||
|
httpMethod string
|
||||||
|
scheme string
|
||||||
|
rootDomain string
|
||||||
|
domain string
|
||||||
|
path string
|
||||||
|
params map[string]string
|
||||||
|
formParams map[string]string
|
||||||
|
|
||||||
|
service string
|
||||||
|
version string
|
||||||
|
action string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) GetAction() string {
|
||||||
|
return r.action
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) GetHttpMethod() string {
|
||||||
|
return r.httpMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) GetParams() map[string]string {
|
||||||
|
return r.params
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) GetPath() string {
|
||||||
|
return r.path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) GetDomain() string {
|
||||||
|
return r.domain
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) GetScheme() string {
|
||||||
|
return r.scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) GetRootDomain() string {
|
||||||
|
return r.rootDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) GetServiceDomain(service string) (domain string) {
|
||||||
|
rootDomain := r.rootDomain
|
||||||
|
if rootDomain == "" {
|
||||||
|
rootDomain = RootDomain
|
||||||
|
}
|
||||||
|
domain = service + "." + rootDomain
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) SetDomain(domain string) {
|
||||||
|
r.domain = domain
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) SetScheme(scheme string) {
|
||||||
|
scheme = strings.ToLower(scheme)
|
||||||
|
switch scheme {
|
||||||
|
case HTTP:
|
||||||
|
r.scheme = HTTP
|
||||||
|
default:
|
||||||
|
r.scheme = HTTPS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) SetRootDomain(rootDomain string) {
|
||||||
|
r.rootDomain = rootDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) SetHttpMethod(method string) {
|
||||||
|
switch strings.ToUpper(method) {
|
||||||
|
case POST:
|
||||||
|
{
|
||||||
|
r.httpMethod = POST
|
||||||
|
}
|
||||||
|
case GET:
|
||||||
|
{
|
||||||
|
r.httpMethod = GET
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
r.httpMethod = GET
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) GetService() string {
|
||||||
|
return r.service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) GetUrl() string {
|
||||||
|
if r.httpMethod == GET {
|
||||||
|
return r.GetScheme() + "://" + r.domain + r.path + "?" + GetUrlQueriesEncoded(r.params)
|
||||||
|
} else if r.httpMethod == POST {
|
||||||
|
return r.GetScheme() + "://" + r.domain + r.path
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) GetVersion() string {
|
||||||
|
return r.version
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUrlQueriesEncoded(params map[string]string) string {
|
||||||
|
values := url.Values{}
|
||||||
|
for key, value := range params {
|
||||||
|
if value != "" {
|
||||||
|
values.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) GetBodyReader() io.Reader {
|
||||||
|
if r.httpMethod == POST {
|
||||||
|
s := GetUrlQueriesEncoded(r.params)
|
||||||
|
return strings.NewReader(s)
|
||||||
|
} else {
|
||||||
|
return strings.NewReader("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) Init() *BaseRequest {
|
||||||
|
r.domain = ""
|
||||||
|
r.path = Path
|
||||||
|
r.params = make(map[string]string)
|
||||||
|
r.formParams = make(map[string]string)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRequest) WithApiInfo(service, version, action string) *BaseRequest {
|
||||||
|
r.service = service
|
||||||
|
r.version = version
|
||||||
|
r.action = action
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated, use request.GetServiceDomain instead
|
||||||
|
func GetServiceDomain(service string) (domain string) {
|
||||||
|
domain = service + "." + RootDomain
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func CompleteCommonParams(request Request, region string) {
|
||||||
|
params := request.GetParams()
|
||||||
|
params["Region"] = region
|
||||||
|
if request.GetVersion() != "" {
|
||||||
|
params["Version"] = request.GetVersion()
|
||||||
|
}
|
||||||
|
params["Action"] = request.GetAction()
|
||||||
|
params["Timestamp"] = strconv.FormatInt(time.Now().Unix(), 10)
|
||||||
|
params["Nonce"] = strconv.Itoa(rand.Int())
|
||||||
|
params["RequestClient"] = "SDK_GO_1.0.62"
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConstructParams(req Request) (err error) {
|
||||||
|
value := reflect.ValueOf(req).Elem()
|
||||||
|
err = flatStructure(value, req, "")
|
||||||
|
//log.Printf("[DEBUG] params=%s", req.GetParams())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func flatStructure(value reflect.Value, request Request, prefix string) (err error) {
|
||||||
|
//log.Printf("[DEBUG] reflect value: %v", value.Type())
|
||||||
|
valueType := value.Type()
|
||||||
|
for i := 0; i < valueType.NumField(); i++ {
|
||||||
|
tag := valueType.Field(i).Tag
|
||||||
|
nameTag, hasNameTag := tag.Lookup("name")
|
||||||
|
if !hasNameTag {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
field := value.Field(i)
|
||||||
|
kind := field.Kind()
|
||||||
|
if kind == reflect.Ptr && field.IsNil() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if kind == reflect.Ptr {
|
||||||
|
field = field.Elem()
|
||||||
|
kind = field.Kind()
|
||||||
|
}
|
||||||
|
key := prefix + nameTag
|
||||||
|
if kind == reflect.String {
|
||||||
|
s := field.String()
|
||||||
|
if s != "" {
|
||||||
|
request.GetParams()[key] = s
|
||||||
|
}
|
||||||
|
} else if kind == reflect.Bool {
|
||||||
|
request.GetParams()[key] = strconv.FormatBool(field.Bool())
|
||||||
|
} else if kind == reflect.Int || kind == reflect.Int64 {
|
||||||
|
request.GetParams()[key] = strconv.FormatInt(field.Int(), 10)
|
||||||
|
} else if kind == reflect.Uint || kind == reflect.Uint64 {
|
||||||
|
request.GetParams()[key] = strconv.FormatUint(field.Uint(), 10)
|
||||||
|
} else if kind == reflect.Float64 {
|
||||||
|
request.GetParams()[key] = strconv.FormatFloat(field.Float(), 'f', -1, 64)
|
||||||
|
} else if kind == reflect.Slice {
|
||||||
|
list := value.Field(i)
|
||||||
|
for j := 0; j < list.Len(); j++ {
|
||||||
|
vj := list.Index(j)
|
||||||
|
key := prefix + nameTag + "." + strconv.Itoa(j)
|
||||||
|
kind = vj.Kind()
|
||||||
|
if kind == reflect.Ptr && vj.IsNil() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if kind == reflect.Ptr {
|
||||||
|
vj = vj.Elem()
|
||||||
|
kind = vj.Kind()
|
||||||
|
}
|
||||||
|
if kind == reflect.String {
|
||||||
|
request.GetParams()[key] = vj.String()
|
||||||
|
} else if kind == reflect.Bool {
|
||||||
|
request.GetParams()[key] = strconv.FormatBool(vj.Bool())
|
||||||
|
} else if kind == reflect.Int || kind == reflect.Int64 {
|
||||||
|
request.GetParams()[key] = strconv.FormatInt(vj.Int(), 10)
|
||||||
|
} else if kind == reflect.Uint || kind == reflect.Uint64 {
|
||||||
|
request.GetParams()[key] = strconv.FormatUint(vj.Uint(), 10)
|
||||||
|
} else if kind == reflect.Float64 {
|
||||||
|
request.GetParams()[key] = strconv.FormatFloat(vj.Float(), 'f', -1, 64)
|
||||||
|
} else {
|
||||||
|
if err = flatStructure(vj, request, key+"."); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err = flatStructure(reflect.ValueOf(field.Interface()), request, prefix+nameTag+"."); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
81
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/response.go
generated
vendored
Normal file
81
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/response.go
generated
vendored
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
//"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Response interface {
|
||||||
|
ParseErrorFromHTTPResponse(body []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseResponse struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Response struct {
|
||||||
|
Error struct {
|
||||||
|
Code string `json:"Code"`
|
||||||
|
Message string `json:"Message"`
|
||||||
|
} `json:"Error,omitempty"`
|
||||||
|
RequestId string `json:"RequestId"`
|
||||||
|
} `json:"Response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeprecatedAPIErrorResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
CodeDesc string `json:"codeDesc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseResponse) ParseErrorFromHTTPResponse(body []byte) (err error) {
|
||||||
|
resp := &ErrorResponse{}
|
||||||
|
err = json.Unmarshal(body, resp)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Fail to parse json content: %s, because: %s", body, err)
|
||||||
|
return errors.NewTencentCloudSDKError("ClientError.ParseJsonError", msg, "")
|
||||||
|
}
|
||||||
|
if resp.Response.Error.Code != "" {
|
||||||
|
return errors.NewTencentCloudSDKError(resp.Response.Error.Code, resp.Response.Error.Message, resp.Response.RequestId)
|
||||||
|
}
|
||||||
|
|
||||||
|
deprecated := &DeprecatedAPIErrorResponse{}
|
||||||
|
err = json.Unmarshal(body, deprecated)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Fail to parse json content: %s, because: %s", body, err)
|
||||||
|
return errors.NewTencentCloudSDKError("ClientError.ParseJsonError", msg, "")
|
||||||
|
}
|
||||||
|
if deprecated.Code != 0 {
|
||||||
|
return errors.NewTencentCloudSDKError(deprecated.CodeDesc, deprecated.Message, "")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFromHttpResponse(hr *http.Response, response Response) (err error) {
|
||||||
|
defer hr.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(hr.Body)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Fail to read response body because %s", err)
|
||||||
|
return errors.NewTencentCloudSDKError("ClientError.IOError", msg, "")
|
||||||
|
}
|
||||||
|
if hr.StatusCode != 200 {
|
||||||
|
msg := fmt.Sprintf("Request fail with http status code: %s, with body: %s", hr.Status, body)
|
||||||
|
return errors.NewTencentCloudSDKError("ClientError.HttpStatusCodeError", msg, "")
|
||||||
|
}
|
||||||
|
//log.Printf("[DEBUG] Response Body=%s", body)
|
||||||
|
err = response.ParseErrorFromHTTPResponse(body)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(body, &response)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Fail to parse json content: %s, because: %s", body, err)
|
||||||
|
return errors.NewTencentCloudSDKError("ClientError.ParseJsonError", msg, "")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
23
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/client_profile.go
generated
vendored
Normal file
23
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/client_profile.go
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package profile
|
||||||
|
|
||||||
|
type ClientProfile struct {
|
||||||
|
HttpProfile *HttpProfile
|
||||||
|
// Valid choices: HmacSHA1, HmacSHA256, TC3-HMAC-SHA256.
|
||||||
|
// Default value is TC3-HMAC-SHA256.
|
||||||
|
SignMethod string
|
||||||
|
UnsignedPayload bool
|
||||||
|
// Valid choices: zh-CN, en-US.
|
||||||
|
// Default value is zh-CN.
|
||||||
|
Language string
|
||||||
|
Debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClientProfile() *ClientProfile {
|
||||||
|
return &ClientProfile{
|
||||||
|
HttpProfile: NewHttpProfile(),
|
||||||
|
SignMethod: "TC3-HMAC-SHA256",
|
||||||
|
UnsignedPayload: false,
|
||||||
|
Language: "zh-CN",
|
||||||
|
Debug: false,
|
||||||
|
}
|
||||||
|
}
|
21
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/http_profile.go
generated
vendored
Normal file
21
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/http_profile.go
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package profile
|
||||||
|
|
||||||
|
type HttpProfile struct {
|
||||||
|
ReqMethod string
|
||||||
|
ReqTimeout int
|
||||||
|
Scheme string
|
||||||
|
RootDomain string
|
||||||
|
Endpoint string
|
||||||
|
// Deprecated, use Scheme instead
|
||||||
|
Protocol string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpProfile() *HttpProfile {
|
||||||
|
return &HttpProfile{
|
||||||
|
ReqMethod: "POST",
|
||||||
|
ReqTimeout: 60,
|
||||||
|
Scheme: "HTTPS",
|
||||||
|
RootDomain: "",
|
||||||
|
Endpoint: "",
|
||||||
|
}
|
||||||
|
}
|
58
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/regions/regions.go
generated
vendored
Normal file
58
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/regions/regions.go
generated
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// Copyright (c) 2018 Tencent Ltd.
|
||||||
|
//
|
||||||
|
// 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 regions
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 曼谷
|
||||||
|
Bangkok = "ap-bangkok"
|
||||||
|
// 北京
|
||||||
|
Beijing = "ap-beijing"
|
||||||
|
// 成都
|
||||||
|
Chengdu = "ap-chengdu"
|
||||||
|
// 重庆
|
||||||
|
Chongqing = "ap-chongqing"
|
||||||
|
// 广州
|
||||||
|
Guangzhou = "ap-guangzhou"
|
||||||
|
// 广州Open
|
||||||
|
GuangzhouOpen = "ap-guangzhou-open"
|
||||||
|
// 中国香港
|
||||||
|
HongKong = "ap-hongkong"
|
||||||
|
// 孟买
|
||||||
|
Mumbai = "ap-mumbai"
|
||||||
|
// 首尔
|
||||||
|
Seoul = "ap-seoul"
|
||||||
|
// 上海
|
||||||
|
Shanghai = "ap-shanghai"
|
||||||
|
// 南京
|
||||||
|
Nanjing = "ap-nanjing"
|
||||||
|
// 上海金融
|
||||||
|
ShanghaiFSI = "ap-shanghai-fsi"
|
||||||
|
// 深圳金融
|
||||||
|
ShenzhenFSI = "ap-shenzhen-fsi"
|
||||||
|
// 新加坡
|
||||||
|
Singapore = "ap-singapore"
|
||||||
|
// 东京
|
||||||
|
Tokyo = "ap-tokyo"
|
||||||
|
// 法兰克福
|
||||||
|
Frankfurt = "eu-frankfurt"
|
||||||
|
// 莫斯科
|
||||||
|
Moscow = "eu-moscow"
|
||||||
|
// 阿什本
|
||||||
|
Ashburn = "na-ashburn"
|
||||||
|
// 硅谷
|
||||||
|
SiliconValley = "na-siliconvalley"
|
||||||
|
// 多伦多
|
||||||
|
Toronto = "na-toronto"
|
||||||
|
)
|
94
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/sign.go
generated
vendored
Normal file
94
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/sign.go
generated
vendored
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SHA256 = "HmacSHA256"
|
||||||
|
SHA1 = "HmacSHA1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Sign(s, secretKey, method string) string {
|
||||||
|
hashed := hmac.New(sha1.New, []byte(secretKey))
|
||||||
|
if method == SHA256 {
|
||||||
|
hashed = hmac.New(sha256.New, []byte(secretKey))
|
||||||
|
}
|
||||||
|
hashed.Write([]byte(s))
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(hashed.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha256hex(s string) string {
|
||||||
|
b := sha256.Sum256([]byte(s))
|
||||||
|
return hex.EncodeToString(b[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func hmacsha256(s, key string) string {
|
||||||
|
hashed := hmac.New(sha256.New, []byte(key))
|
||||||
|
hashed.Write([]byte(s))
|
||||||
|
return string(hashed.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func signRequest(request tchttp.Request, credential *Credential, method string) (err error) {
|
||||||
|
if method != SHA256 {
|
||||||
|
method = SHA1
|
||||||
|
}
|
||||||
|
checkAuthParams(request, credential, method)
|
||||||
|
s := getStringToSign(request)
|
||||||
|
signature := Sign(s, credential.SecretKey, method)
|
||||||
|
request.GetParams()["Signature"] = signature
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAuthParams(request tchttp.Request, credential *Credential, method string) {
|
||||||
|
params := request.GetParams()
|
||||||
|
credentialParams := credential.GetCredentialParams()
|
||||||
|
for key, value := range credentialParams {
|
||||||
|
params[key] = value
|
||||||
|
}
|
||||||
|
params["SignatureMethod"] = method
|
||||||
|
delete(params, "Signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStringToSign(request tchttp.Request) string {
|
||||||
|
method := request.GetHttpMethod()
|
||||||
|
domain := request.GetDomain()
|
||||||
|
path := request.GetPath()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString(method)
|
||||||
|
buf.WriteString(domain)
|
||||||
|
buf.WriteString(path)
|
||||||
|
buf.WriteString("?")
|
||||||
|
|
||||||
|
params := request.GetParams()
|
||||||
|
// sort params
|
||||||
|
keys := make([]string, 0, len(params))
|
||||||
|
for k, _ := range params {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for i := range keys {
|
||||||
|
k := keys[i]
|
||||||
|
// TODO: check if server side allows empty value in url.
|
||||||
|
if params[k] == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buf.WriteString(k)
|
||||||
|
buf.WriteString("=")
|
||||||
|
buf.WriteString(params[k])
|
||||||
|
buf.WriteString("&")
|
||||||
|
}
|
||||||
|
buf.Truncate(buf.Len() - 1)
|
||||||
|
return buf.String()
|
||||||
|
}
|
95
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/types.go
generated
vendored
Normal file
95
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/types.go
generated
vendored
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
func IntPtr(v int) *int {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int64Ptr(v int64) *int64 {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func UintPtr(v uint) *uint {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func Uint64Ptr(v uint64) *uint64 {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func Float64Ptr(v float64) *float64 {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func BoolPtr(v bool) *bool {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringPtr(v string) *string {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringValues(ptrs []*string) []string {
|
||||||
|
values := make([]string, len(ptrs))
|
||||||
|
for i := 0; i < len(ptrs); i++ {
|
||||||
|
if ptrs[i] != nil {
|
||||||
|
values[i] = *ptrs[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntPtrs(vals []int) []*int {
|
||||||
|
ptrs := make([]*int, len(vals))
|
||||||
|
for i := 0; i < len(vals); i++ {
|
||||||
|
ptrs[i] = &vals[i]
|
||||||
|
}
|
||||||
|
return ptrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func Int64Ptrs(vals []int64) []*int64 {
|
||||||
|
ptrs := make([]*int64, len(vals))
|
||||||
|
for i := 0; i < len(vals); i++ {
|
||||||
|
ptrs[i] = &vals[i]
|
||||||
|
}
|
||||||
|
return ptrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func UintPtrs(vals []uint) []*uint {
|
||||||
|
ptrs := make([]*uint, len(vals))
|
||||||
|
for i := 0; i < len(vals); i++ {
|
||||||
|
ptrs[i] = &vals[i]
|
||||||
|
}
|
||||||
|
return ptrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func Uint64Ptrs(vals []uint64) []*uint64 {
|
||||||
|
ptrs := make([]*uint64, len(vals))
|
||||||
|
for i := 0; i < len(vals); i++ {
|
||||||
|
ptrs[i] = &vals[i]
|
||||||
|
}
|
||||||
|
return ptrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func Float64Ptrs(vals []float64) []*float64 {
|
||||||
|
ptrs := make([]*float64, len(vals))
|
||||||
|
for i := 0; i < len(vals); i++ {
|
||||||
|
ptrs[i] = &vals[i]
|
||||||
|
}
|
||||||
|
return ptrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func BoolPtrs(vals []bool) []*bool {
|
||||||
|
ptrs := make([]*bool, len(vals))
|
||||||
|
for i := 0; i < len(vals); i++ {
|
||||||
|
ptrs[i] = &vals[i]
|
||||||
|
}
|
||||||
|
return ptrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringPtrs(vals []string) []*string {
|
||||||
|
ptrs := make([]*string, len(vals))
|
||||||
|
for i := 0; i < len(vals); i++ {
|
||||||
|
ptrs[i] = &vals[i]
|
||||||
|
}
|
||||||
|
return ptrs
|
||||||
|
}
|
1519
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tcr/v20190924/client.go
generated
vendored
Normal file
1519
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tcr/v20190924/client.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3222
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tcr/v20190924/models.go
generated
vendored
Normal file
3222
src/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tcr/v20190924/models.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
8
src/vendor/modules.txt
vendored
8
src/vendor/modules.txt
vendored
@ -477,6 +477,14 @@ github.com/stretchr/testify/assert
|
|||||||
github.com/stretchr/testify/mock
|
github.com/stretchr/testify/mock
|
||||||
github.com/stretchr/testify/require
|
github.com/stretchr/testify/require
|
||||||
github.com/stretchr/testify/suite
|
github.com/stretchr/testify/suite
|
||||||
|
# github.com/tencentcloud/tencentcloud-sdk-go v1.0.62
|
||||||
|
## explicit
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/regions
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tcr/v20190924
|
||||||
# github.com/theupdateframework/notary v0.6.1
|
# github.com/theupdateframework/notary v0.6.1
|
||||||
## explicit
|
## explicit
|
||||||
github.com/theupdateframework/notary
|
github.com/theupdateframework/notary
|
||||||
|
Loading…
Reference in New Issue
Block a user