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:
疯魔慕薇 2020-03-25 16:44:12 +08:00 committed by Ziming
parent ec2f251d63
commit 9f8a743da9
27 changed files with 7588 additions and 0 deletions

View File

@ -66,6 +66,7 @@ require (
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect
github.com/spf13/viper v1.4.0 // indirect
github.com/stretchr/testify v1.5.1
github.com/tencentcloud/tencentcloud-sdk-go v1.0.62
github.com/theupdateframework/notary v0.6.1
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/net v0.0.0-20200625001655-4c5254603344

View File

@ -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/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/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/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY=
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=

View File

@ -54,6 +54,8 @@ import (
_ "github.com/goharbor/harbor/src/replication/adapter/dtr"
// register the Artifact Hub adapter
_ "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

View 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
}

View 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)
}

View 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)
}

View 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)
}
})
}
}

View 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
}

View 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)
}

View 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
}

View 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)
}
})
}
}

View File

@ -32,6 +32,7 @@ const (
RegistryTypeQuay RegistryType = "quay"
RegistryTypeGitLab RegistryType = "gitlab"
RegistryTypeDTR RegistryType = "dtr"
RegistryTypeTencentTcr RegistryType = "tencent-tcr"
RegistryTypeHelmHub RegistryType = "helm-hub"
RegistryTypeArtifactHub RegistryType = "artifact-hub"

View File

@ -59,6 +59,8 @@ import (
_ "github.com/goharbor/harbor/src/replication/adapter/dtr"
// register the Artifact Hub adapter
_ "github.com/goharbor/harbor/src/replication/adapter/artifacthub"
// register the TencentCloud TCR adapter
_ "github.com/goharbor/harbor/src/replication/adapter/tencentcr"
)
var (

View 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.

View 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
}

View 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,
// }
//}

View 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
}

View 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
}

View 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
}

View 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,
}
}

View 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: "",
}
}

View 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"
)

View 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()
}

View 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
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -477,6 +477,14 @@ github.com/stretchr/testify/assert
github.com/stretchr/testify/mock
github.com/stretchr/testify/require
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
## explicit
github.com/theupdateframework/notary