mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-29 13:45:20 +01:00
Implement replication adapter API
This commit implements the replication adapter API Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
parent
c9498410a9
commit
7f49151115
@ -2080,6 +2080,53 @@ paths:
|
|||||||
$ref: '#/responses/UnsupportedMediaType'
|
$ref: '#/responses/UnsupportedMediaType'
|
||||||
'500':
|
'500':
|
||||||
description: Unexpected internal errors.
|
description: Unexpected internal errors.
|
||||||
|
/replication/adapters:
|
||||||
|
get:
|
||||||
|
summary: List supported adapters.
|
||||||
|
description: |
|
||||||
|
This endpoint let user list supported adapters.
|
||||||
|
tags:
|
||||||
|
- Products
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Success.
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/ReplicationAdapter'
|
||||||
|
'401':
|
||||||
|
description: Unauthorized.
|
||||||
|
'403':
|
||||||
|
description: Forbidden.
|
||||||
|
'500':
|
||||||
|
description: Unexpected internal errors.
|
||||||
|
/replication/adapters/{type}:
|
||||||
|
get:
|
||||||
|
summary: Get the specified adapter.
|
||||||
|
description: |
|
||||||
|
This endpoint let user get the specified adapter.
|
||||||
|
parameters:
|
||||||
|
- name: type
|
||||||
|
in: path
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: The adapter type.
|
||||||
|
tags:
|
||||||
|
- Products
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Success.
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
$ref: '#/definitions/ReplicationAdapter'
|
||||||
|
'401':
|
||||||
|
description: Unauthorized.
|
||||||
|
'403':
|
||||||
|
description: Forbidden.
|
||||||
|
'404':
|
||||||
|
description: Not found.
|
||||||
|
'500':
|
||||||
|
description: Unexpected internal errors.
|
||||||
/registries:
|
/registries:
|
||||||
get:
|
get:
|
||||||
summary: List registries.
|
summary: List registries.
|
||||||
@ -4746,3 +4793,23 @@ definitions:
|
|||||||
action:
|
action:
|
||||||
type: string
|
type: string
|
||||||
description: The permission action
|
description: The permission action
|
||||||
|
ReplicationAdapter:
|
||||||
|
type: object
|
||||||
|
description: The replication adapter
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
description: The adapter type
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
description: The adapter description
|
||||||
|
supported_resource_types:
|
||||||
|
type: array
|
||||||
|
description: The resource types that the adapter supports
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
supported_resource_filters:
|
||||||
|
type: array
|
||||||
|
description: The filters that the adapter supports
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
@ -151,6 +151,9 @@ func init() {
|
|||||||
beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List")
|
beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List")
|
||||||
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete")
|
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete")
|
||||||
|
|
||||||
|
beego.Router("/api/replication/adapters", &ReplicationAdapterAPI{}, "get:List")
|
||||||
|
beego.Router("/api/replication/adapters/:type", &ReplicationAdapterAPI{}, "get:Get")
|
||||||
|
|
||||||
// Charts are controlled under projects
|
// Charts are controlled under projects
|
||||||
chartRepositoryAPIType := &ChartRepositoryAPI{}
|
chartRepositoryAPIType := &ChartRepositoryAPI{}
|
||||||
beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus")
|
beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus")
|
||||||
|
57
src/core/api/replication_adapter.go
Normal file
57
src/core/api/replication_adapter.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright 2018 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 api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/replication/ng/adapter"
|
||||||
|
"github.com/goharbor/harbor/src/replication/ng/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReplicationAdapterAPI handles the replication adapter requests
|
||||||
|
type ReplicationAdapterAPI struct {
|
||||||
|
BaseController
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare ...
|
||||||
|
func (r *ReplicationAdapterAPI) Prepare() {
|
||||||
|
r.BaseController.Prepare()
|
||||||
|
if !r.SecurityCtx.IsSysAdmin() {
|
||||||
|
if !r.SecurityCtx.IsAuthenticated() {
|
||||||
|
r.HandleUnauthorized()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.HandleForbidden(r.SecurityCtx.GetUsername())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the replication adapters
|
||||||
|
func (r *ReplicationAdapterAPI) List() {
|
||||||
|
infos := adapter.ListAdapterInfos()
|
||||||
|
r.WriteJSONData(infos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get one specified replication adapter
|
||||||
|
func (r *ReplicationAdapterAPI) Get() {
|
||||||
|
t := r.GetStringFromPath(":type")
|
||||||
|
info := adapter.GetAdapterInfo(model.RegistryType(t))
|
||||||
|
if info == nil {
|
||||||
|
r.HandleNotFound(fmt.Sprintf("adapter for %s not found", t))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.WriteJSONData(info)
|
||||||
|
}
|
110
src/core/api/replication_adapter_test.go
Normal file
110
src/core/api/replication_adapter_test.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// Copyright 2018 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 api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/replication/ng/adapter"
|
||||||
|
"github.com/goharbor/harbor/src/replication/ng/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReplicationAdapterAPIList(t *testing.T) {
|
||||||
|
cases := []*codeCheckingCase{
|
||||||
|
// 401
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/adapters",
|
||||||
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
// 403
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/adapters",
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
// 200
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/adapters",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runCodeCheckingCases(t, cases...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fakedFactory(*model.Registry) (adapter.Adapter, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func TestReplicationAdapterAPIGet(t *testing.T) {
|
||||||
|
err := adapter.RegisterFactory(
|
||||||
|
&adapter.Info{
|
||||||
|
Type: "harbor",
|
||||||
|
SupportedResourceTypes: []model.ResourceType{"image"},
|
||||||
|
}, fakedFactory)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
cases := []*codeCheckingCase{
|
||||||
|
// 401
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/adapters/harbor",
|
||||||
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
// 403
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/adapters/harbor",
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
// 404
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/adapters/gcs",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
// 200
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/adapters/harbor",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runCodeCheckingCases(t, cases...)
|
||||||
|
}
|
@ -99,6 +99,9 @@ func initRouters() {
|
|||||||
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "post:Post")
|
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "post:Post")
|
||||||
beego.Router("/api/logs", &api.LogAPI{})
|
beego.Router("/api/logs", &api.LogAPI{})
|
||||||
|
|
||||||
|
beego.Router("/api/replication/adapters", &api.ReplicationAdapterAPI{}, "get:List")
|
||||||
|
beego.Router("/api/replication/adapters/:type", &api.ReplicationAdapterAPI{}, "get:Get")
|
||||||
|
|
||||||
beego.Router("/api/internal/configurations", &api.ConfigAPI{}, "get:GetInternalConfig;put:Put")
|
beego.Router("/api/internal/configurations", &api.ConfigAPI{}, "get:GetInternalConfig;put:Put")
|
||||||
beego.Router("/api/configurations", &api.ConfigAPI{}, "get:Get;put:Put")
|
beego.Router("/api/configurations", &api.ConfigAPI{}, "get:Get;put:Put")
|
||||||
beego.Router("/api/statistics", &api.StatisticAPI{})
|
beego.Router("/api/statistics", &api.StatisticAPI{})
|
||||||
|
@ -21,23 +21,28 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/replication/ng/model"
|
"github.com/goharbor/harbor/src/replication/ng/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
var registry = map[model.RegistryType]Factory{}
|
// As the Info isn't a valid map key, so we use the slice
|
||||||
|
// as the adapter registry
|
||||||
|
var registry = []*item{}
|
||||||
|
|
||||||
// Factory creates a specific Adapter according to the params
|
type item struct {
|
||||||
type Factory func(*model.Registry) (Adapter, error)
|
info *Info
|
||||||
|
factory Factory
|
||||||
|
}
|
||||||
|
|
||||||
// Info provides base info and capability declarations of the adapter
|
// Info provides base info and capability declarations of the adapter
|
||||||
type Info struct {
|
type Info struct {
|
||||||
Name model.RegistryType `json:"name"`
|
Type model.RegistryType `json:"type"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
SupportedResourceTypes []model.ResourceType `json:"supported_resource_types"`
|
SupportedResourceTypes []model.ResourceType `json:"supported_resource_types"`
|
||||||
SupportedResourceFilters []model.FilterType `json:"supported_resource_filters"`
|
SupportedResourceFilters []model.FilterType `json:"supported_resource_filters"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Factory creates a specific Adapter according to the params
|
||||||
|
type Factory func(*model.Registry) (Adapter, error)
|
||||||
|
|
||||||
// Adapter interface defines the capabilities of registry
|
// Adapter interface defines the capabilities of registry
|
||||||
type Adapter interface {
|
type Adapter interface {
|
||||||
// Info return the information of this adapter
|
|
||||||
Info() *Info
|
|
||||||
// Lists the available namespaces under the specified registry with the
|
// Lists the available namespaces under the specified registry with the
|
||||||
// provided credential/token
|
// provided credential/token
|
||||||
ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace, error)
|
ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace, error)
|
||||||
@ -55,26 +60,53 @@ type Adapter interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RegisterFactory registers one adapter factory to the registry
|
// RegisterFactory registers one adapter factory to the registry
|
||||||
func RegisterFactory(name model.RegistryType, factory Factory) error {
|
func RegisterFactory(info *Info, factory Factory) error {
|
||||||
if !name.Valid() {
|
if len(info.Type) == 0 {
|
||||||
return errors.New("invalid adapter factory name")
|
return errors.New("invalid registry type")
|
||||||
|
}
|
||||||
|
if len(info.SupportedResourceTypes) == 0 {
|
||||||
|
return errors.New("must support at least one resource type")
|
||||||
}
|
}
|
||||||
if factory == nil {
|
if factory == nil {
|
||||||
return errors.New("empty adapter factory")
|
return errors.New("empty adapter factory")
|
||||||
}
|
}
|
||||||
if _, exist := registry[name]; exist {
|
for _, item := range registry {
|
||||||
return fmt.Errorf("adapter factory for %s already exists", name)
|
if item.info.Type == info.Type {
|
||||||
|
return fmt.Errorf("adapter factory for %s already exists", info.Type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
registry[name] = factory
|
registry = append(registry, &item{
|
||||||
|
info: info,
|
||||||
|
factory: factory,
|
||||||
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFactory gets the adapter factory by the specified name
|
// GetFactory gets the adapter factory by the specified name
|
||||||
func GetFactory(name model.RegistryType) (Factory, error) {
|
func GetFactory(t model.RegistryType) (Factory, error) {
|
||||||
factory, exist := registry[name]
|
for _, item := range registry {
|
||||||
if !exist {
|
if item.info.Type == t {
|
||||||
return nil, fmt.Errorf("adapter factory for %s not found", name)
|
return item.factory, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return nil, fmt.Errorf("adapter factory for %s not found", t)
|
||||||
return factory, nil
|
}
|
||||||
|
|
||||||
|
// ListAdapterInfos lists the info of registered Adapters
|
||||||
|
func ListAdapterInfos() []*Info {
|
||||||
|
infos := []*Info{}
|
||||||
|
for _, item := range registry {
|
||||||
|
infos = append(infos, item.info)
|
||||||
|
}
|
||||||
|
return infos
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAdapterInfo returns the info of a specified registry type
|
||||||
|
func GetAdapterInfo(t model.RegistryType) *Info {
|
||||||
|
for _, item := range registry {
|
||||||
|
if item.info.Type == t {
|
||||||
|
return item.info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -27,23 +27,80 @@ func fakedFactory(*model.Registry) (Adapter, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisterFactory(t *testing.T) {
|
func TestRegisterFactory(t *testing.T) {
|
||||||
// empty name
|
// empty type
|
||||||
assert.NotNil(t, RegisterFactory("", nil))
|
assert.NotNil(t, RegisterFactory(&Info{}, nil))
|
||||||
|
// empty supportted resource type
|
||||||
|
assert.NotNil(t, RegisterFactory(
|
||||||
|
&Info{
|
||||||
|
Type: "harbor",
|
||||||
|
}, nil))
|
||||||
// empty factory
|
// empty factory
|
||||||
assert.NotNil(t, RegisterFactory("factory", nil))
|
assert.NotNil(t, RegisterFactory(
|
||||||
|
&Info{
|
||||||
|
Type: "harbor",
|
||||||
|
SupportedResourceTypes: []model.ResourceType{"image"},
|
||||||
|
}, nil))
|
||||||
// pass
|
// pass
|
||||||
assert.Nil(t, RegisterFactory("factory", fakedFactory))
|
assert.Nil(t, RegisterFactory(
|
||||||
|
&Info{
|
||||||
|
Type: "harbor",
|
||||||
|
SupportedResourceTypes: []model.ResourceType{"image"},
|
||||||
|
}, fakedFactory))
|
||||||
// already exists
|
// already exists
|
||||||
assert.NotNil(t, RegisterFactory("factory", fakedFactory))
|
assert.NotNil(t, RegisterFactory(
|
||||||
|
&Info{
|
||||||
|
Type: "harbor",
|
||||||
|
SupportedResourceTypes: []model.ResourceType{"image"},
|
||||||
|
}, fakedFactory))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetFactory(t *testing.T) {
|
func TestGetFactory(t *testing.T) {
|
||||||
registry = map[model.RegistryType]Factory{}
|
registry = []*item{}
|
||||||
require.Nil(t, RegisterFactory("factory", fakedFactory))
|
require.Nil(t, RegisterFactory(
|
||||||
|
&Info{
|
||||||
|
Type: "harbor",
|
||||||
|
SupportedResourceTypes: []model.ResourceType{"image"},
|
||||||
|
}, fakedFactory))
|
||||||
// doesn't exist
|
// doesn't exist
|
||||||
_, err := GetFactory("another_factory")
|
_, err := GetFactory("gcr")
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
// pass
|
// pass
|
||||||
_, err = GetFactory("factory")
|
_, err = GetFactory("harbor")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListAdapterInfos(t *testing.T) {
|
||||||
|
registry = []*item{}
|
||||||
|
// not register, got nothing
|
||||||
|
infos := ListAdapterInfos()
|
||||||
|
assert.Equal(t, 0, len(infos))
|
||||||
|
|
||||||
|
// register one factory
|
||||||
|
require.Nil(t, RegisterFactory(
|
||||||
|
&Info{
|
||||||
|
Type: "harbor",
|
||||||
|
SupportedResourceTypes: []model.ResourceType{"image"},
|
||||||
|
}, fakedFactory))
|
||||||
|
|
||||||
|
infos = ListAdapterInfos()
|
||||||
|
require.Equal(t, 1, len(infos))
|
||||||
|
assert.Equal(t, "harbor", string(infos[0].Type))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAdapterInfo(t *testing.T) {
|
||||||
|
registry = []*item{}
|
||||||
|
require.Nil(t, RegisterFactory(
|
||||||
|
&Info{
|
||||||
|
Type: "harbor",
|
||||||
|
SupportedResourceTypes: []model.ResourceType{"image"},
|
||||||
|
}, fakedFactory))
|
||||||
|
|
||||||
|
// doesn't exist
|
||||||
|
info := GetAdapterInfo("gcr")
|
||||||
|
assert.Nil(t, info)
|
||||||
|
|
||||||
|
// exist
|
||||||
|
info = GetAdapterInfo("harbor")
|
||||||
|
require.NotNil(t, info)
|
||||||
|
assert.Equal(t, "harbor", string(info.Type))
|
||||||
|
}
|
||||||
|
@ -185,7 +185,11 @@ func (f *fakedAdapter) FetchResources(namespace []string, filters []*model.Filte
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStartReplication(t *testing.T) {
|
func TestStartReplication(t *testing.T) {
|
||||||
err := adapter.RegisterFactory("faked_registry", fakedAdapterFactory)
|
err := adapter.RegisterFactory(
|
||||||
|
&adapter.Info{
|
||||||
|
Type: "faked_registry",
|
||||||
|
SupportedResourceTypes: []model.ResourceType{"image"},
|
||||||
|
}, fakedAdapterFactory)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
controller, _ := NewController(
|
controller, _ := NewController(
|
||||||
|
@ -20,18 +20,13 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegistryType indicates the type of registry
|
|
||||||
type RegistryType string
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// RegistryTypeHarbor indicates registry type harbor
|
// RegistryTypeHarbor indicates registry type harbor
|
||||||
RegistryTypeHarbor = "harbor"
|
RegistryTypeHarbor = "harbor"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Valid indicates whether the RegistryType is a valid value
|
// RegistryType indicates the type of registry
|
||||||
func (r RegistryType) Valid() bool {
|
type RegistryType string
|
||||||
return len(r) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// CredentialType represents the supported credential types
|
// CredentialType represents the supported credential types
|
||||||
// e.g: u/p, OAuth token
|
// e.g: u/p, OAuth token
|
||||||
|
Loading…
Reference in New Issue
Block a user