From e3849164e3e0d84a19844f453012aeda1b4d4428 Mon Sep 17 00:00:00 2001 From: njucjc Date: Thu, 22 Feb 2024 16:22:56 +0800 Subject: [PATCH] feat: add acr ee support Signed-off-by: njucjc --- src/go.mod | 2 +- src/go.sum | 2 + src/pkg/reg/adapter/aliacr/adapter.go | 230 ++++++------- src/pkg/reg/adapter/aliacr/adapter_test.go | 119 ++++--- src/pkg/reg/adapter/aliacr/auth.go | 40 +-- src/pkg/reg/adapter/aliacr/openapi.go | 306 ++++++++++++++++++ src/pkg/reg/adapter/aliacr/types.go | 14 +- .../create-edit-endpoint.component.spec.ts | 160 +++++++++ .../registries/endpoint.component.spec.ts | 160 +++++++++ 9 files changed, 823 insertions(+), 210 deletions(-) create mode 100644 src/pkg/reg/adapter/aliacr/openapi.go diff --git a/src/go.mod b/src/go.mod index e8a84ace4..e8aa423d6 100644 --- a/src/go.mod +++ b/src/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/FZambia/sentinel v1.1.0 github.com/Masterminds/semver v1.5.0 - github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190726115642-cd293c93fd97 + github.com/aliyun/alibaba-cloud-sdk-go v1.61.1193 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/aws/aws-sdk-go v1.50.5 github.com/beego/beego/v2 v2.0.6 diff --git a/src/go.sum b/src/go.sum index 1824a5427..1f0babaef 100644 --- a/src/go.sum +++ b/src/go.sum @@ -66,6 +66,8 @@ github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3Uu github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190726115642-cd293c93fd97 h1:bNE5ID4C3YOkROfvBjXJUG53gyb+8az3TQN02LqnGBk= github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190726115642-cd293c93fd97/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ= +github.com/aliyun/alibaba-cloud-sdk-go v1.61.1193 h1:C5LuIDWuQlugv30EBsSLKFF6jdtrqoVH84nYCdVYTC4= +github.com/aliyun/alibaba-cloud-sdk-go v1.61.1193/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA= github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= diff --git a/src/pkg/reg/adapter/aliacr/adapter.go b/src/pkg/reg/adapter/aliacr/adapter.go index a0c66d949..67f0728ea 100644 --- a/src/pkg/reg/adapter/aliacr/adapter.go +++ b/src/pkg/reg/adapter/aliacr/adapter.go @@ -15,16 +15,12 @@ package aliacr import ( - "encoding/json" "errors" "fmt" "path/filepath" "regexp" "strings" - "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" - "github.com/aliyun/alibaba-cloud-sdk-go/services/cr" - commonhttp "github.com/goharbor/harbor/src/common/http" "github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/lib/log" @@ -45,48 +41,83 @@ func init() { } // example: -// https://registry.%s.aliyuncs.com // https://cr.%s.aliyuncs.com -// https://registry-vpc.%s.aliyuncs.com -// https://registry-internal.%s.aliyuncs.com -var regRegion = regexp.MustCompile(`https://(registry|cr|registry-vpc|registry-internal)\.([\w\-]+)\.aliyuncs\.com`) +var regACRServiceURL = regexp.MustCompile(`https://cr\.([\w\-]+)\.aliyuncs\.com`) -func getRegion(url string) (region string, err error) { +func getURL(url string) (string, error) { if url == "" { return "", errors.New("empty url") } - rs := regRegion.FindStringSubmatch(url) + + var rs []string + rs = regACRServiceURL.FindStringSubmatch(url) if rs == nil { - return "", errors.New("invalid Rgistry|CR service url") + return url, nil + } + return fmt.Sprintf(registryEndpointTpl, rs[1]), nil +} + +// example: +// registry.aliyuncs.com:cn-hangzhou:china:cri-xxxxxxxxx +// registry.aliyuncs.com:cn-hangzhou:26842 +func parseRegistryService(service string) (*registryServiceInfo, error) { + parts := strings.Split(service, ":") + length := len(parts) + if length <= 0 { + return nil, errors.New("empty service") + } + + if !strings.EqualFold(parts[0], registryACRService) { + return nil, errors.New("not a acr service") + } + + if strings.HasPrefix(parts[length-1], "cri-") { + return ®istryServiceInfo{ + IsACREE: true, + RegionID: parts[1], + InstanceID: parts[length-1], + }, nil + } else { + return ®istryServiceInfo{ + IsACREE: false, + RegionID: parts[1], + }, nil } - // fmt.Println(rs) - return rs[2], nil } func newAdapter(registry *model.Registry) (*adapter, error) { - region, err := getRegion(registry.URL) + url, err := getURL(registry.URL) if err != nil { return nil, err } - switch true { - case strings.Contains(registry.URL, "registry-vpc"): - registry.URL = fmt.Sprintf(registryVPCEndpointTpl, region) - case strings.Contains(registry.URL, "registry-internal"): - registry.URL = fmt.Sprintf(registryInternalEndpointTpl, region) - default: - // fix url (allow user input cr service url) - registry.URL = fmt.Sprintf(registryEndpointTpl, region) - } + registry.URL = url + realm, service, err := util.Ping(registry) if err != nil { return nil, err } - credential := NewAuth(region, registry.Credential.AccessKey, registry.Credential.AccessSecret) - authorizer := bearer.NewAuthorizer(realm, service, credential, commonhttp.GetHTTPTransport(commonhttp.WithInsecure(registry.Insecure))) + + info, err := parseRegistryService(service) + if err != nil { + return nil, err + } + + var acrAPI openapi + if !info.IsACREE { + acrAPI, err = newAcrOpenapi(registry.Credential.AccessKey, registry.Credential.AccessSecret, info.RegionID) + if err != nil { + return nil, err + } + } else { + acrAPI, err = newAcreeOpenapi(registry.Credential.AccessKey, registry.Credential.AccessSecret, info.RegionID, info.InstanceID) + if err != nil { + return nil, err + } + } + authorizer := bearer.NewAuthorizer(realm, service, NewAuth(acrAPI), commonhttp.GetHTTPTransport(commonhttp.WithInsecure(registry.Insecure))) return &adapter{ - region: region, + acrAPI: acrAPI, registry: registry, - domain: fmt.Sprintf(endpointTpl, region), Adapter: native.NewAdapterWithAuthorizer(registry, authorizer), }, nil } @@ -112,16 +143,15 @@ var ( // adapter for to aliyun docker registry type adapter struct { *native.Adapter - region string - domain string + acrAPI openapi registry *model.Registry } var _ adp.Adapter = &adapter{} // Info ... -func (a *adapter) Info() (info *model.RegistryInfo, err error) { - info = &model.RegistryInfo{ +func (a *adapter) Info() (*model.RegistryInfo, error) { + info := &model.RegistryInfo{ Type: model.RegistryTypeAliAcr, SupportedResourceTypes: []string{ model.ResourceTypeImage, @@ -141,7 +171,7 @@ func (a *adapter) Info() (info *model.RegistryInfo, err error) { model.TriggerTypeScheduled, }, } - return + return info, nil } func getAdapterInfo() *model.AdapterPattern { @@ -184,6 +214,16 @@ func getAdapterInfo() *model.AdapterPattern { Key: e + "-internal", Value: fmt.Sprintf("https://registry-internal.%s.aliyuncs.com", e), }) + + endpoints = append(endpoints, &model.Endpoint{ + Key: e + "-ee-vpc", + Value: fmt.Sprintf("https://instanceName-registry-vpc.%s.cr.aliyuncs.com", e), + }) + + endpoints = append(endpoints, &model.Endpoint{ + Key: e + "-ee", + Value: fmt.Sprintf("https://instanceName-registry.%s.cr.aliyuncs.com", e), + }) } info := &model.AdapterPattern{ EndpointPattern: &model.EndpointPattern{ @@ -194,30 +234,8 @@ func getAdapterInfo() *model.AdapterPattern { return info } -func (a *adapter) listNamespaces(c *cr.Client) (namespaces []string, err error) { - // list namespaces - var nsReq = cr.CreateGetNamespaceListRequest() - var nsResp *cr.GetNamespaceListResponse - nsReq.SetDomain(a.domain) - nsResp, err = c.GetNamespaceList(nsReq) - if err != nil { - return - } - var resp = &aliACRNamespaceResp{} - err = json.Unmarshal(nsResp.GetHttpContentBytes(), resp) - if err != nil { - return - } - for _, ns := range resp.Data.Namespaces { - namespaces = append(namespaces, ns.Namespace) - } - - log.Debugf("FetchArtifacts.listNamespaces: %#v\n", namespaces) - - return -} - -func (a *adapter) listCandidateNamespaces(c *cr.Client, namespacePattern string) (namespaces []string, err error) { +func (a *adapter) listCandidateNamespaces(namespacePattern string) ([]string, error) { + var namespaces []string if len(namespacePattern) > 0 { if nms, ok := util.IsSpecificPathComponent(namespacePattern); ok { namespaces = append(namespaces, nms...) @@ -228,19 +246,22 @@ func (a *adapter) listCandidateNamespaces(c *cr.Client, namespacePattern string) } } - return a.listNamespaces(c) + if a.acrAPI == nil { + return nil, errors.New("acr api is nil") + } + + return a.acrAPI.ListNamespace() } // FetchArtifacts AliACR not support /v2/_catalog of Registry, we'll list all resources via Aliyun's API -func (a *adapter) FetchArtifacts(filters []*model.Filter) (resources []*model.Resource, err error) { +func (a *adapter) FetchArtifacts(filters []*model.Filter) ([]*model.Resource, error) { log.Debugf("FetchArtifacts.filters: %#v\n", filters) - var client *cr.Client - client, err = cr.NewClientWithAccessKey(a.region, a.registry.Credential.AccessKey, a.registry.Credential.AccessSecret) - if err != nil { - return + if a.acrAPI == nil { + return nil, errors.New("acr api is nil") } + var resources []*model.Resource // get filter pattern var repoPattern string var tagsPattern string @@ -254,31 +275,29 @@ func (a *adapter) FetchArtifacts(filters []*model.Filter) (resources []*model.Re log.Debugf("\nrepoPattern=%s tagsPattern=%s\n\n", repoPattern, tagsPattern) // get namespaces - var namespaces []string - namespaces, err = a.listCandidateNamespaces(client, namespacePattern) + namespaces, err := a.listCandidateNamespaces(namespacePattern) if err != nil { - return + return nil, err } log.Debugf("got namespaces: %v \n", namespaces) // list repos - var repositories []aliRepo + var repositories []*repository for _, namespace := range namespaces { - var repos []aliRepo - repos, err = a.listReposByNamespace(a.region, namespace, client) + repos, err := a.acrAPI.ListRepository(namespace) if err != nil { - return + return nil, err } log.Debugf("\nnamespace: %s \t repositories: %#v\n\n", namespace, repos) for _, repo := range repos { var ok bool - var repoName = filepath.Join(repo.RepoNamespace, repo.RepoName) + var repoName = filepath.Join(repo.Namespace, repo.Name) ok, err = util.Match(repoPattern, repoName) log.Debugf("\n Repository: %s\t repoPattern: %s\t Match: %v\n", repoName, repoPattern, ok) if err != nil { - return + return nil, err } if ok { repositories = append(repositories, repo) @@ -295,9 +314,9 @@ func (a *adapter) FetchArtifacts(filters []*model.Filter) (resources []*model.Re repo := r runner.AddTask(func() error { var tags []string - tags, err = a.getTags(repo, client) + tags, err = a.acrAPI.ListRepoTag(repo) if err != nil { - return fmt.Errorf("list tags for repo '%s' error: %v", repo.RepoName, err) + return fmt.Errorf("list tags for repo '%s' error: %v", repo.Name, err) } var artifacts []*model.Artifact @@ -317,13 +336,12 @@ func (a *adapter) FetchArtifacts(filters []*model.Filter) (resources []*model.Re Registry: a.registry, Metadata: &model.ResourceMetadata{ Repository: &model.Repository{ - Name: filepath.Join(repo.RepoNamespace, repo.RepoName), + Name: filepath.Join(repo.Namespace, repo.Name), }, Artifacts: filterArtifacts, }, } } - return nil }) } @@ -336,65 +354,5 @@ func (a *adapter) FetchArtifacts(filters []*model.Filter) (resources []*model.Re } } - return -} - -func (a *adapter) listReposByNamespace(_ string, namespace string, c *cr.Client) (repos []aliRepo, err error) { - var reposReq = cr.CreateGetRepoListByNamespaceRequest() - var reposResp = cr.CreateGetRepoListByNamespaceResponse() - reposReq.SetDomain(a.domain) - reposReq.RepoNamespace = namespace - var page = 1 - for { - reposReq.Page = requests.NewInteger(page) - reposResp, err = c.GetRepoListByNamespace(reposReq) - if err != nil { - return - } - var resp = &aliReposResp{} - err = json.Unmarshal(reposResp.GetHttpContentBytes(), resp) - if err != nil { - return - } - repos = append(repos, resp.Data.Repos...) - - if resp.Data.Total-(resp.Data.Page*resp.Data.PageSize) <= 0 { - break - } - page++ - } - return -} - -func (a *adapter) getTags(repo aliRepo, c *cr.Client) (tags []string, err error) { - log.Debugf("[ali-acr.getTags]%s: %#v\n", a.domain, repo) - var tagsReq = cr.CreateGetRepoTagsRequest() - var tagsResp = cr.CreateGetRepoTagsResponse() - tagsReq.SetDomain(a.domain) - tagsReq.RepoNamespace = repo.RepoNamespace - tagsReq.RepoName = repo.RepoName - var page = 1 - for { - tagsReq.Page = requests.NewInteger(page) - tagsResp, err = c.GetRepoTags(tagsReq) - if err != nil { - return - } - - var resp = &aliTagResp{} - err = json.Unmarshal(tagsResp.GetHttpContentBytes(), resp) - if err != nil { - return - } - for _, tag := range resp.Data.Tags { - tags = append(tags, tag.Tag) - } - - if resp.Data.Total-(resp.Data.Page*resp.Data.PageSize) <= 0 { - break - } - page++ - } - - return + return resources, nil } diff --git a/src/pkg/reg/adapter/aliacr/adapter_test.go b/src/pkg/reg/adapter/aliacr/adapter_test.go index e62238a9c..c412681f9 100644 --- a/src/pkg/reg/adapter/aliacr/adapter_test.go +++ b/src/pkg/reg/adapter/aliacr/adapter_test.go @@ -62,8 +62,6 @@ func getMockAdapter(t *testing.T, hasCred, health bool) (*adapter, *httptest.Ser } return &adapter{ Adapter: native.NewAdapter(registry), - region: "test-region", - domain: server.URL, registry: registry, }, server } @@ -79,46 +77,91 @@ func TestAdapter_Info(t *testing.T) { assert.EqualValues(t, model.ResourceTypeImage, info.SupportedResourceTypes[0]) } -func Test_getRegion(t *testing.T) { +func Test_getURL(t *testing.T) { tests := []struct { - name string - url string - wantRegion string - wantErr bool + name string + url string + want string + wantErr bool }{ - {"registry shanghai", "https://registry.cn-shanghai.aliyuncs.com", "cn-shanghai", false}, - {"invalid registry shanghai", "http://registry.cn-shanghai.aliyuncs.com", "", true}, - {"registry hangzhou", "https://registry.cn-hangzhou.aliyuncs.com", "cn-hangzhou", false}, - {"registry hangzhou vpc", "https://registry-vpc.cn-hangzhou.aliyuncs.com", "cn-hangzhou", false}, - {"registry hangzhou internal", "https://registry-internal.cn-hangzhou.aliyuncs.com", "cn-hangzhou", false}, - {"cr shanghai", "https://cr.cn-shanghai.aliyuncs.com", "cn-shanghai", false}, - {"cr hangzhou", "https://cr.cn-hangzhou.aliyuncs.com", "cn-hangzhou", false}, - {"invalid cr url", "https://acr.cn-hangzhou.aliyuncs.com", "", true}, - {"invalid registry url", "https://registry.cn-hangzhou.ali.com", "", true}, + { + "empty url", + "", + "", + true, + }, + { + "just return url", + "https://cr.cn-hangzhou.aliyun.com", + "https://cr.cn-hangzhou.aliyun.com", + false, + }, + { + "change match url", + "https://cr.cn-hangzhou.aliyuncs.com", + "https://registry.cn-hangzhou.aliyuncs.com", + false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotRegion, err := getRegion(tt.url) + got, err := getURL(tt.url) if tt.wantErr { assert.NotNil(t, err) } - assert.Equal(t, tt.wantRegion, gotRegion) + assert.Equal(t, tt.want, got) }) } } -var urlForBenchmark = []string{ - "https://cr.cn-hangzhou.aliyuncs.com", - "https://registry.cn-shanghai.aliyuncs.com", - "https://registry-vpc.cn-shanghai.aliyuncs.com", - "https://registry-internal.cn-shanghai.aliyuncs.com", -} - -func BenchmarkGetRegion(b *testing.B) { - for i := 0; i < b.N; i++ { - for _, url := range urlForBenchmark { - getRegion(url) - } +func Test_parseRegistryService(t *testing.T) { + tests := []struct { + name string + service string + wantInfo *registryServiceInfo + wantErr bool + }{ + { + "not acr Service", + "otherregistry.cn-hangzhou:china", + nil, + true, + }, + { + "empty Service", + "", + nil, + true, + }, + { + "acr ee service", + "registry.aliyuncs.com:cn-hangzhou:china:cri-xxxxxxxxx", + ®istryServiceInfo{ + IsACREE: true, + RegionID: "cn-hangzhou", + InstanceID: "cri-xxxxxxxxx", + }, + false, + }, + { + "acr service", + "registry.aliyuncs.com:cn-hangzhou:26842", + ®istryServiceInfo{ + IsACREE: false, + RegionID: "cn-hangzhou", + InstanceID: "", + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + info, err := parseRegistryService(tt.service) + if tt.wantErr { + assert.NotNil(t, err) + } + assert.Equal(t, tt.wantInfo, info) + }) } } @@ -132,9 +175,6 @@ func Test_adapter_FetchArtifacts(t *testing.T) { } func Test_aliyunAuthCredential_isCacheTokenValid(t *testing.T) { type fields struct { - region string - accessKey string - secretKey string cacheToken *registryTemporaryToken cacheTokenExpiredAt time.Time } @@ -145,23 +185,22 @@ func Test_aliyunAuthCredential_isCacheTokenValid(t *testing.T) { fields fields want bool }{ - {"nil cacheTokenExpiredAt", fields{"test-region", "MockAccessKey", "MockSecretKey", nil, nilTime}, false}, - {"nil cacheToken", fields{"test-region", "MockAccessKey", "MockSecretKey", nil, time.Time{}}, false}, - {"expired", fields{"test-region", "MockAccessKey", "MockSecretKey", ®istryTemporaryToken{}, time.Now().AddDate(0, 0, -1)}, false}, - {"ok", fields{"test-region", "MockAccessKey", "MockSecretKey", ®istryTemporaryToken{}, time.Now().AddDate(0, 0, 1)}, true}, + {"nil cacheTokenExpiredAt", fields{nil, nilTime}, false}, + {"nil cacheToken", fields{nil, time.Time{}}, false}, + {"expired", fields{®istryTemporaryToken{}, time.Now().AddDate(0, 0, -1)}, false}, + {"ok", fields{®istryTemporaryToken{}, time.Now().AddDate(0, 0, 1)}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &aliyunAuthCredential{ - region: tt.fields.region, - accessKey: tt.fields.accessKey, - secretKey: tt.fields.secretKey, cacheToken: tt.fields.cacheToken, cacheTokenExpiredAt: tt.fields.cacheTokenExpiredAt, } if got := a.isCacheTokenValid(); got != tt.want { + fmt.Println(got) assert.Equal(t, got, tt.want) } }) } + } diff --git a/src/pkg/reg/adapter/aliacr/auth.go b/src/pkg/reg/adapter/aliacr/auth.go index 375b1510a..38f006d5e 100644 --- a/src/pkg/reg/adapter/aliacr/auth.go +++ b/src/pkg/reg/adapter/aliacr/auth.go @@ -15,13 +15,10 @@ package aliacr import ( - "encoding/json" - "fmt" + "errors" "net/http" "time" - "github.com/aliyun/alibaba-cloud-sdk-go/services/cr" - "github.com/goharbor/harbor/src/common/http/modifier" "github.com/goharbor/harbor/src/lib/log" ) @@ -31,9 +28,7 @@ type Credential modifier.Modifier // Implements interface Credential type aliyunAuthCredential struct { - region string - accessKey string - secretKey string + acrAPI openapi cacheToken *registryTemporaryToken cacheTokenExpiredAt time.Time } @@ -46,11 +41,9 @@ type registryTemporaryToken struct { var _ Credential = &aliyunAuthCredential{} // NewAuth will get a temporary docker registry username and password via aliyun cr service API. -func NewAuth(region, accessKey, secretKey string) Credential { +func NewAuth(acrAPI openapi) Credential { return &aliyunAuthCredential{ - region: region, - accessKey: accessKey, - secretKey: secretKey, + acrAPI: acrAPI, cacheToken: ®istryTemporaryToken{}, } } @@ -58,27 +51,16 @@ func NewAuth(region, accessKey, secretKey string) Credential { func (a *aliyunAuthCredential) Modify(r *http.Request) (err error) { if !a.isCacheTokenValid() { log.Debugf("[aliyunAuthCredential.Modify.updateToken]Host: %s\n", r.Host) - var client *cr.Client - client, err = cr.NewClientWithAccessKey(a.region, a.accessKey, a.secretKey) - if err != nil { - return + if a.acrAPI == nil { + return errors.New("acr api is nil") } - - var tokenRequest = cr.CreateGetAuthorizationTokenRequest() - var tokenResponse *cr.GetAuthorizationTokenResponse - tokenRequest.SetDomain(fmt.Sprintf(endpointTpl, a.region)) - tokenResponse, err = client.GetAuthorizationToken(tokenRequest) + v, err := a.acrAPI.GetAuthorizationToken() if err != nil { - return + return err } - var v authorizationToken - err = json.Unmarshal(tokenResponse.GetHttpContentBytes(), &v) - if err != nil { - return - } - a.cacheTokenExpiredAt = v.Data.ExpireDate.ToTime() - a.cacheToken.user = v.Data.TempUserName - a.cacheToken.password = v.Data.AuthorizationToken + a.cacheTokenExpiredAt = v.expiresAt + a.cacheToken.user = v.user + a.cacheToken.password = v.password } else { log.Debug("[aliyunAuthCredential] USE CACHE TOKEN!!!") } diff --git a/src/pkg/reg/adapter/aliacr/openapi.go b/src/pkg/reg/adapter/aliacr/openapi.go new file mode 100644 index 000000000..f7df5c8a6 --- /dev/null +++ b/src/pkg/reg/adapter/aliacr/openapi.go @@ -0,0 +1,306 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aliacr + +import ( + "encoding/json" + "fmt" + "strconv" + "time" + + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" + "github.com/aliyun/alibaba-cloud-sdk-go/services/cr" + "github.com/aliyun/alibaba-cloud-sdk-go/services/cr_ee" +) + +type repository struct { + Name string + Namespace string + ID string +} + +type authToken struct { + user string + password string + expiresAt time.Time +} + +// openapi is an interface that defines methods for interacting with an open API. +type openapi interface { + // ListNamespace returns a list of all namespaces. + ListNamespace() ([]string, error) + // ListRepository returns a list of all repositories for a specified namespace. + ListRepository(namespaceName string) ([]*repository, error) + // ListRepoTag returns a list of all tags for a specified repository. + ListRepoTag(repo *repository) ([]string, error) + // GetAuthorizationToken returns the authorization token for repository. + GetAuthorizationToken() (*authToken, error) +} + +type acrOpenapi struct { + client *cr.Client + domain string +} + +var _ openapi = &acrOpenapi{} + +// newAcrOpenapi creates a new acrOpenapi instance. +func newAcrOpenapi(accessKeyID string, accessKeySecret string, regionID string) (openapi, error) { + client, err := cr.NewClientWithAccessKey(regionID, accessKeyID, accessKeySecret) + if err != nil { + return nil, err + } + return &acrOpenapi{ + client: client, + domain: fmt.Sprintf(endpointTpl, regionID), + }, nil +} + +// ListNamespace returns a list of namespaces +func (acr *acrOpenapi) ListNamespace() ([]string, error) { + var namespaces []string + var nsReq = cr.CreateGetNamespaceListRequest() + var nsResp *cr.GetNamespaceListResponse + nsReq.SetDomain(acr.domain) + nsResp, err := acr.client.GetNamespaceList(nsReq) + if err != nil { + return nil, err + } + var resp = &aliACRNamespaceResp{} + err = json.Unmarshal(nsResp.GetHttpContentBytes(), resp) + if err != nil { + return nil, err + } + for _, ns := range resp.Data.Namespaces { + namespaces = append(namespaces, ns.Namespace) + } + return namespaces, nil +} + +// ListRepository returns a list of repositories in the specified namespace +func (acr *acrOpenapi) ListRepository(namespaceName string) ([]*repository, error) { + var repos []*repository + var reposReq = cr.CreateGetRepoListByNamespaceRequest() + reposReq.SetDomain(acr.domain) + reposReq.RepoNamespace = namespaceName + var page = 1 + for { + reposReq.Page = requests.NewInteger(page) + reposResp, err := acr.client.GetRepoListByNamespace(reposReq) + if err != nil { + return nil, err + } + var resp = &aliReposResp{} + err = json.Unmarshal(reposResp.GetHttpContentBytes(), resp) + if err != nil { + return nil, err + } + + for _, repo := range resp.Data.Repos { + repos = append(repos, &repository{ + Name: repo.RepoName, + Namespace: repo.RepoNamespace, + }) + } + + if resp.Data.Total-(resp.Data.Page*resp.Data.PageSize) <= 0 { + break + } + page++ + } + return repos, nil +} + +// ListRepoTag returns a list of tags in the specified repository +func (acr *acrOpenapi) ListRepoTag(repo *repository) ([]string, error) { + var tags []string + var tagsReq = cr.CreateGetRepoTagsRequest() + tagsReq.SetDomain(acr.domain) + tagsReq.RepoNamespace = repo.Namespace + tagsReq.RepoName = repo.Name + var page = 1 + for { + tagsReq.Page = requests.NewInteger(page) + tagsResp, err := acr.client.GetRepoTags(tagsReq) + if err != nil { + return nil, err + } + var resp = &aliTagResp{} + err = json.Unmarshal(tagsResp.GetHttpContentBytes(), resp) + if err != nil { + return nil, err + } + for _, tag := range resp.Data.Tags { + tags = append(tags, tag.Tag) + } + + if resp.Data.Total-(resp.Data.Page*resp.Data.PageSize) <= 0 { + break + } + page++ + } + return tags, nil +} + +// GetAuthorizationToken returns the authorization token for repository. +func (acr *acrOpenapi) GetAuthorizationToken() (*authToken, error) { + var tokenRequest = cr.CreateGetAuthorizationTokenRequest() + var tokenResponse *cr.GetAuthorizationTokenResponse + tokenRequest.SetDomain(acr.domain) + tokenResponse, err := acr.client.GetAuthorizationToken(tokenRequest) + if err != nil { + return nil, err + } + var v authorizationToken + err = json.Unmarshal(tokenResponse.GetHttpContentBytes(), &v) + if err != nil { + return nil, err + } + return &authToken{ + user: v.Data.TempUserName, + password: v.Data.AuthorizationToken, + expiresAt: v.Data.ExpireDate.ToTime(), + }, nil +} + +type acreeOpenapi struct { + client *cr_ee.Client + domain string + instanceID string +} + +var _ openapi = &acreeOpenapi{} + +// newAcreeOpenapi creates a new acreeOpenapi instance. +func newAcreeOpenapi(accessKeyID string, accessKeySecret string, regionID string, instanceID string) (openapi, error) { + client, err := cr_ee.NewClientWithAccessKey(regionID, accessKeyID, accessKeySecret) + if err != nil { + return nil, err + } + return &acreeOpenapi{ + client: client, + domain: fmt.Sprintf(endpointTpl, regionID), + instanceID: instanceID, + }, nil +} + +// ListNamespace returns a list of namespaces +func (acree *acreeOpenapi) ListNamespace() ([]string, error) { + var namespaces []string + var nsReq = cr_ee.CreateListNamespaceRequest() + nsReq.SetDomain(acree.domain) + nsReq.InstanceId = acree.instanceID + var page = 1 + for { + nsReq.PageNo = requests.NewInteger(page) + nsResp, err := acree.client.ListNamespace(nsReq) + if err != nil { + return nil, err + } + for _, ns := range nsResp.Namespaces { + namespaces = append(namespaces, ns.NamespaceName) + } + total, err := strconv.Atoi(nsResp.TotalCount) + if err != nil { + return nil, err + } + if total-(nsResp.PageNo*nsResp.PageSize) <= 0 { + break + } + page++ + } + return namespaces, nil +} + +// ListRepository returns a list of repositories in the specified namespace +func (acree *acreeOpenapi) ListRepository(namespaceName string) ([]*repository, error) { + var repos []*repository + var reposReq = cr_ee.CreateListRepositoryRequest() + reposReq.SetDomain(acree.domain) + reposReq.InstanceId = acree.instanceID + reposReq.RepoNamespaceName = namespaceName + reposReq.RepoStatus = "NORMAL" + var page = 1 + for { + reposReq.PageNo = requests.NewInteger(page) + reposResp, err := acree.client.ListRepository(reposReq) + if err != nil { + return nil, err + } + for _, repo := range reposResp.Repositories { + repos = append(repos, &repository{ + Name: repo.RepoName, + Namespace: repo.RepoNamespaceName, + ID: repo.RepoId, + }) + } + + total, err := strconv.Atoi(reposResp.TotalCount) + if err != nil { + return nil, err + } + if total-(reposResp.PageNo*reposResp.PageSize) <= 0 { + break + } + page++ + } + return repos, nil +} + +// ListRepoTag returns a list of tags in the specified repository +func (acree *acreeOpenapi) ListRepoTag(repo *repository) ([]string, error) { + var tags []string + var tagsReq = cr_ee.CreateListRepoTagRequest() + tagsReq.SetDomain(acree.domain) + tagsReq.InstanceId = acree.instanceID + tagsReq.RepoId = repo.ID + var page = 1 + for { + tagsReq.PageNo = requests.NewInteger(page) + tagsResp, err := acree.client.ListRepoTag(tagsReq) + if err != nil { + return nil, err + } + for _, image := range tagsResp.Images { + tags = append(tags, image.Tag) + } + total, err := strconv.Atoi(tagsResp.TotalCount) + if err != nil { + return nil, err + } + if total-(tagsResp.PageNo*tagsResp.PageSize) <= 0 { + break + } + page++ + } + return tags, nil +} + +// GetAuthorizationToken returns the authorization token for repository. +func (acree *acreeOpenapi) GetAuthorizationToken() (*authToken, error) { + var tokenRequest = cr_ee.CreateGetAuthorizationTokenRequest() + // FIXME: use vpc endpoint if vpc is enabled + tokenRequest.SetDomain(acree.domain) + tokenRequest.InstanceId = acree.instanceID + tokenResponse, err := acree.client.GetAuthorizationToken(tokenRequest) + if err != nil { + return nil, err + } + return &authToken{ + user: tokenResponse.TempUsername, + password: tokenResponse.AuthorizationToken, + expiresAt: time.Unix(tokenResponse.ExpireTime/1000, 0), + }, nil +} diff --git a/src/pkg/reg/adapter/aliacr/types.go b/src/pkg/reg/adapter/aliacr/types.go index 6ff30cca3..7d918282b 100644 --- a/src/pkg/reg/adapter/aliacr/types.go +++ b/src/pkg/reg/adapter/aliacr/types.go @@ -17,12 +17,18 @@ package aliacr import "time" const ( - registryEndpointTpl = "https://registry.%s.aliyuncs.com" - registryVPCEndpointTpl = "https://registry-vpc.%s.aliyuncs.com" - registryInternalEndpointTpl = "https://registry-internal.%s.aliyuncs.com" - endpointTpl = "cr.%s.aliyuncs.com" + registryEndpointTpl = "https://registry.%s.aliyuncs.com" + endpointTpl = "cr.%s.aliyuncs.com" + + registryACRService = "registry.aliyuncs.com" ) +type registryServiceInfo struct { + IsACREE bool + RegionID string + InstanceID string +} + type authorizationToken struct { Data struct { ExpireDate timeUnix `json:"expireDate"` diff --git a/src/portal/src/app/base/left-side-nav/registries/create-edit-endpoint/create-edit-endpoint.component.spec.ts b/src/portal/src/app/base/left-side-nav/registries/create-edit-endpoint/create-edit-endpoint.component.spec.ts index f75982a52..4d571c7d8 100644 --- a/src/portal/src/app/base/left-side-nav/registries/create-edit-endpoint/create-edit-endpoint.component.spec.ts +++ b/src/portal/src/app/base/left-side-nav/registries/create-edit-endpoint/create-edit-endpoint.component.spec.ts @@ -273,6 +273,166 @@ describe('CreateEditEndpointComponent (inline template)', () => { key: 'me-east-1-internal', value: 'https://registry-internal.me-east-1.aliyuncs.com', }, + { + key: 'cn-hangzhou-ee-vpc', + value: `https://instanceName-registry-vpc.cn-hangzhou.cr.aliyuncs.com`, + }, + { + key: 'cn-shanghai-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-shanghai.cr.aliyuncs.com', + }, + { + key: 'cn-qingdao-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-qingdao.cr.aliyuncs.com', + }, + { + key: 'cn-beijing-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-beijing.cr.aliyuncs.com', + }, + { + key: 'cn-zhangjiakou-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-zhangjiakou.cr.aliyuncs.com', + }, + { + key: 'cn-huhehaote-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-huhehaote.cr.aliyuncs.com', + }, + { + key: 'cn-shenzhen-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-shenzhen.cr.aliyuncs.com', + }, + { + key: 'cn-chengdu-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-chengdu.cr.aliyuncs.com', + }, + { + key: 'cn-hongkong-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-hongkong.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-1-ee-vpc', + value: 'https://instanceName-registry-vpc.ap-southeast-1.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-2-ee-vpc', + value: 'https://instanceName-registry-vpc.ap-southeast-2.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-3-ee-vpc', + value: 'https://instanceName-registry-vpc.ap-southeast-3.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-5-ee-vpc', + value: 'https://instanceName-registry-vpc.ap-southeast-5.aliyuncs.cr.com', + }, + { + key: 'ap-northeast-1-ee-vpc', + value: 'https://instanceName-registry-vpc.ap-northeast-1.cr.aliyuncs.com', + }, + { + key: 'ap-south-1-ee-vpc', + value: 'https:/instanceName-/registry-vpc.ap-south-1.cr.aliyuncs.com', + }, + { + key: 'eu-central-1-ee-vpc', + value: 'https://instanceName-registry-vpc.eu-central-1.cr.aliyuncs.com', + }, + { + key: 'eu-west-1-ee-vpc', + value: 'https://instanceName-registry-vpc.eu-west-1.cr.aliyuncs.com', + }, + { + key: 'us-west-1-ee-vpc', + value: 'https://instanceName-registry-vpc.us-west-1.cr.aliyuncs.com', + }, + { + key: 'us-east-1-ee-vpc', + value: 'https://instanceName-registry-vpc.us-east-1.cr.aliyuncs.com', + }, + { + key: 'me-east-1-ee-vpc', + value: 'https://instanceName-registry-vpc.me-east-1.cr.aliyuncs.com', + }, + { + key: 'cn-hangzhou-ee', + value: `https://instanceName-registry.cn-hangzhou.cr.aliyuncs.com`, + }, + { + key: 'cn-shanghai-ee', + value: 'https://instanceName-registry.cn-shanghai.cr.aliyuncs.com', + }, + { + key: 'cn-qingdao-ee', + value: 'https://instanceName-registry.cn-qingdao.cr.aliyuncs.com', + }, + { + key: 'cn-beijing-ee', + value: 'https://instanceName-registry.cn-beijing.cr.aliyuncs.com', + }, + { + key: 'cn-zhangjiakou-ee', + value: 'https://instanceName-registry.cn-zhangjiakou.cr.aliyuncs.com', + }, + { + key: 'cn-huhehaote-ee', + value: 'https://instanceName-registry.cn-huhehaote.cr.aliyuncs.com', + }, + { + key: 'cn-shenzhen-ee', + value: 'https://instanceName-registry.cn-shenzhen.cr.aliyuncs.com', + }, + { + key: 'cn-chengdu-ee', + value: 'https://instanceName-registry.cn-chengdu.cr.aliyuncs.com', + }, + { + key: 'cn-hongkong-ee', + value: 'https://instanceName-registry.cn-hongkong.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-1-ee', + value: 'https://instanceName-registry.ap-southeast-1.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-2-ee', + value: 'https://instanceName-registry.ap-southeast-2.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-3-ee', + value: 'https://instanceName-registry.ap-southeast-3.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-5-ee', + value: 'https://instanceName-registry.ap-southeast-5.aliyuncs.cr.com', + }, + { + key: 'ap-northeast-1-ee', + value: 'https://instanceName-registry.ap-northeast-1.cr.aliyuncs.com', + }, + { + key: 'ap-south-1-ee', + value: 'https:/instanceName-/registry.ap-south-1.cr.aliyuncs.com', + }, + { + key: 'eu-central-1-ee', + value: 'https://instanceName-registry.eu-central-1.cr.aliyuncs.com', + }, + { + key: 'eu-west-1-ee', + value: 'https://instanceName-registry.eu-west-1.cr.aliyuncs.com', + }, + { + key: 'us-west-1-ee', + value: 'https://instanceName-registry.us-west-1.cr.aliyuncs.com', + }, + { + key: 'us-east-1-ee', + value: 'https://instanceName-registry-.us-east-1.cr.aliyuncs.com', + }, + { + key: 'me-east-1-ee', + value: 'https://instanceName-registry.me-east-1.cr.aliyuncs.com', + }, ], }, credential_pattern: null, diff --git a/src/portal/src/app/base/left-side-nav/registries/endpoint.component.spec.ts b/src/portal/src/app/base/left-side-nav/registries/endpoint.component.spec.ts index 473a1de75..1b649347c 100644 --- a/src/portal/src/app/base/left-side-nav/registries/endpoint.component.spec.ts +++ b/src/portal/src/app/base/left-side-nav/registries/endpoint.component.spec.ts @@ -263,6 +263,166 @@ describe('EndpointComponent (inline template)', () => { key: 'me-east-1-internal', value: 'https://registry-internal.me-east-1.aliyuncs.com', }, + { + key: 'cn-hangzhou-ee-vpc', + value: `https://instanceName-registry-vpc.cn-hangzhou.cr.aliyuncs.com`, + }, + { + key: 'cn-shanghai-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-shanghai.cr.aliyuncs.com', + }, + { + key: 'cn-qingdao-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-qingdao.cr.aliyuncs.com', + }, + { + key: 'cn-beijing-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-beijing.cr.aliyuncs.com', + }, + { + key: 'cn-zhangjiakou-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-zhangjiakou.cr.aliyuncs.com', + }, + { + key: 'cn-huhehaote-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-huhehaote.cr.aliyuncs.com', + }, + { + key: 'cn-shenzhen-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-shenzhen.cr.aliyuncs.com', + }, + { + key: 'cn-chengdu-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-chengdu.cr.aliyuncs.com', + }, + { + key: 'cn-hongkong-ee-vpc', + value: 'https://instanceName-registry-vpc.cn-hongkong.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-1-ee-vpc', + value: 'https://instanceName-registry-vpc.ap-southeast-1.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-2-ee-vpc', + value: 'https://instanceName-registry-vpc.ap-southeast-2.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-3-ee-vpc', + value: 'https://instanceName-registry-vpc.ap-southeast-3.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-5-ee-vpc', + value: 'https://instanceName-registry-vpc.ap-southeast-5.aliyuncs.cr.com', + }, + { + key: 'ap-northeast-1-ee-vpc', + value: 'https://instanceName-registry-vpc.ap-northeast-1.cr.aliyuncs.com', + }, + { + key: 'ap-south-1-ee-vpc', + value: 'https:/instanceName-/registry-vpc.ap-south-1.cr.aliyuncs.com', + }, + { + key: 'eu-central-1-ee-vpc', + value: 'https://instanceName-registry-vpc.eu-central-1.cr.aliyuncs.com', + }, + { + key: 'eu-west-1-ee-vpc', + value: 'https://instanceName-registry-vpc.eu-west-1.cr.aliyuncs.com', + }, + { + key: 'us-west-1-ee-vpc', + value: 'https://instanceName-registry-vpc.us-west-1.cr.aliyuncs.com', + }, + { + key: 'us-east-1-ee-vpc', + value: 'https://instanceName-registry-vpc.us-east-1.cr.aliyuncs.com', + }, + { + key: 'me-east-1-ee-vpc', + value: 'https://instanceName-registry-vpc.me-east-1.cr.aliyuncs.com', + }, + { + key: 'cn-hangzhou-ee', + value: `https://instanceName-registry.cn-hangzhou.cr.aliyuncs.com`, + }, + { + key: 'cn-shanghai-ee', + value: 'https://instanceName-registry.cn-shanghai.cr.aliyuncs.com', + }, + { + key: 'cn-qingdao-ee', + value: 'https://instanceName-registry.cn-qingdao.cr.aliyuncs.com', + }, + { + key: 'cn-beijing-ee', + value: 'https://instanceName-registry.cn-beijing.cr.aliyuncs.com', + }, + { + key: 'cn-zhangjiakou-ee', + value: 'https://instanceName-registry.cn-zhangjiakou.cr.aliyuncs.com', + }, + { + key: 'cn-huhehaote-ee', + value: 'https://instanceName-registry.cn-huhehaote.cr.aliyuncs.com', + }, + { + key: 'cn-shenzhen-ee', + value: 'https://instanceName-registry.cn-shenzhen.cr.aliyuncs.com', + }, + { + key: 'cn-chengdu-ee', + value: 'https://instanceName-registry.cn-chengdu.cr.aliyuncs.com', + }, + { + key: 'cn-hongkong-ee', + value: 'https://instanceName-registry.cn-hongkong.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-1-ee', + value: 'https://instanceName-registry.ap-southeast-1.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-2-ee', + value: 'https://instanceName-registry.ap-southeast-2.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-3-ee', + value: 'https://instanceName-registry.ap-southeast-3.cr.aliyuncs.com', + }, + { + key: 'ap-southeast-5-ee', + value: 'https://instanceName-registry.ap-southeast-5.aliyuncs.cr.com', + }, + { + key: 'ap-northeast-1-ee', + value: 'https://instanceName-registry.ap-northeast-1.cr.aliyuncs.com', + }, + { + key: 'ap-south-1-ee', + value: 'https:/instanceName-/registry.ap-south-1.cr.aliyuncs.com', + }, + { + key: 'eu-central-1-ee', + value: 'https://instanceName-registry.eu-central-1.cr.aliyuncs.com', + }, + { + key: 'eu-west-1-ee', + value: 'https://instanceName-registry.eu-west-1.cr.aliyuncs.com', + }, + { + key: 'us-west-1-ee', + value: 'https://instanceName-registry.us-west-1.cr.aliyuncs.com', + }, + { + key: 'us-east-1-ee', + value: 'https://instanceName-registry-.us-east-1.cr.aliyuncs.com', + }, + { + key: 'me-east-1-ee', + value: 'https://instanceName-registry.me-east-1.cr.aliyuncs.com', + }, ], }, credential_pattern: null,