mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-14 22:35:36 +01:00
Support artifact hub replication using new API
Signed-off-by: peimingming <peimingming@corp.netease.com>
This commit is contained in:
parent
839c36c876
commit
28714f8b70
@ -68,7 +68,7 @@ type ArtifactRegistry interface {
|
|||||||
type ChartRegistry interface {
|
type ChartRegistry interface {
|
||||||
FetchCharts(filters []*model.Filter) ([]*model.Resource, error)
|
FetchCharts(filters []*model.Filter) ([]*model.Resource, error)
|
||||||
ChartExist(name, version string) (bool, error)
|
ChartExist(name, version string) (bool, error)
|
||||||
DownloadChart(name, version string) (io.ReadCloser, error)
|
DownloadChart(name, version, contentURL string) (io.ReadCloser, error)
|
||||||
UploadChart(name, version string, chart io.Reader) error
|
UploadChart(name, version string, chart io.Reader) error
|
||||||
DeleteChart(name, version string) error
|
DeleteChart(name, version string) error
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,11 @@ func TestAdapter_DownloadChart(t *testing.T) {
|
|||||||
URL: "https://artifacthub.io",
|
URL: "https://artifacthub.io",
|
||||||
})
|
})
|
||||||
|
|
||||||
data, err := a.DownloadChart("harbor/harbor", "1.5.0")
|
data, err := a.DownloadChart("harbor/harbor", "1.5.0", "")
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Nil(t, data)
|
||||||
|
|
||||||
|
data, err = a.DownloadChart("harbor/harbor", "1.5.0", "https://helm.goharbor.io/harbor-1.5.0.tgz")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.NotNil(t, data)
|
assert.NotNil(t, data)
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ package artifacthub
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
|
||||||
"github.com/goharbor/harbor/src/replication/filter"
|
"github.com/goharbor/harbor/src/replication/filter"
|
||||||
"github.com/goharbor/harbor/src/replication/model"
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
"io"
|
"io"
|
||||||
@ -26,62 +25,92 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (a *adapter) FetchCharts(filters []*model.Filter) ([]*model.Resource, error) {
|
func (a *adapter) FetchCharts(filters []*model.Filter) ([]*model.Resource, error) {
|
||||||
pkgs, err := a.client.getAllPackages(HelmChart)
|
charts, err := a.client.getReplicationInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resources := []*model.Resource{}
|
resources := []*model.Resource{}
|
||||||
var repositories []*model.Repository
|
var repositories []*model.Repository
|
||||||
for _, pkg := range pkgs {
|
var artifacts []*model.Artifact
|
||||||
repositories = append(repositories, &model.Repository{
|
repoSet := map[string]interface{}{}
|
||||||
Name: fmt.Sprintf("%s/%s", pkg.Repository.Name, pkg.Name),
|
versionSet := map[string]interface{}{}
|
||||||
})
|
for _, chart := range charts {
|
||||||
|
name := fmt.Sprintf("%s/%s", chart.Repository, chart.Package)
|
||||||
|
if _, ok := repoSet[name]; !ok {
|
||||||
|
repoSet[name] = nil
|
||||||
|
repositories = append(repositories, &model.Repository{
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories, err = filter.DoFilterRepositories(repositories, filters)
|
repositories, err = filter.DoFilterRepositories(repositories, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(repositories) == 0 {
|
||||||
|
return resources, nil
|
||||||
|
}
|
||||||
|
|
||||||
for _, repository := range repositories {
|
if len(repoSet) != len(repositories) {
|
||||||
pkgDetail, err := a.client.getHelmPackageDetail(repository.Name)
|
repoSet = map[string]interface{}{}
|
||||||
if err != nil {
|
for _, repo := range repositories {
|
||||||
log.Errorf("fetch package detail: %v", err)
|
repoSet[repo.Name] = nil
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var artifacts []*model.Artifact
|
for _, chart := range charts {
|
||||||
for _, version := range pkgDetail.AvailableVersions {
|
name := fmt.Sprintf("%s/%s", chart.Repository, chart.Package)
|
||||||
|
if _, ok := repoSet[name]; ok {
|
||||||
artifacts = append(artifacts, &model.Artifact{
|
artifacts = append(artifacts, &model.Artifact{
|
||||||
Tags: []string{version.Version},
|
Tags: []string{chart.Version},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
artifacts, err = filter.DoFilterArtifacts(artifacts, filters)
|
artifacts, err = filter.DoFilterArtifacts(artifacts, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(artifacts) == 0 {
|
if len(artifacts) == 0 {
|
||||||
|
return resources, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arti := range artifacts {
|
||||||
|
versionSet[arti.Tags[0]] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, chart := range charts {
|
||||||
|
name := fmt.Sprintf("%s/%s", chart.Repository, chart.Package)
|
||||||
|
if _, ok := repoSet[name]; !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if _, ok := versionSet[chart.Version]; !ok {
|
||||||
for _, artifact := range artifacts {
|
continue
|
||||||
resources = append(resources, &model.Resource{
|
|
||||||
Type: model.ResourceTypeChart,
|
|
||||||
Registry: a.registry,
|
|
||||||
Metadata: &model.ResourceMetadata{
|
|
||||||
Repository: &model.Repository{
|
|
||||||
Name: repository.Name,
|
|
||||||
},
|
|
||||||
Artifacts: []*model.Artifact{artifact},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
resources = append(resources, &model.Resource{
|
||||||
|
Type: model.ResourceTypeChart,
|
||||||
|
Registry: a.registry,
|
||||||
|
Metadata: &model.ResourceMetadata{
|
||||||
|
Repository: &model.Repository{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
Artifacts: []*model.Artifact{
|
||||||
|
{
|
||||||
|
Tags: []string{chart.Version},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExtendedInfo: map[string]interface{}{
|
||||||
|
"contentURL": chart.ContentURL,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return resources, nil
|
return resources, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChartExist will never be used, for this function is only used when endpoint is destination
|
||||||
func (a *adapter) ChartExist(name, version string) (bool, error) {
|
func (a *adapter) ChartExist(name, version string) (bool, error) {
|
||||||
_, err := a.client.getHelmChartVersion(name, version)
|
_, err := a.client.getHelmChartVersion(name, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -93,16 +122,11 @@ func (a *adapter) ChartExist(name, version string) (bool, error) {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *adapter) DownloadChart(name, version string) (io.ReadCloser, error) {
|
func (a *adapter) DownloadChart(name, version, contentURL string) (io.ReadCloser, error) {
|
||||||
chartVersion, err := a.client.getHelmChartVersion(name, version)
|
if len(contentURL) == 0 {
|
||||||
if err != nil {
|
return nil, errors.Errorf("empty chart content url, %s:%s", name, version)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
return a.download(contentURL)
|
||||||
if len(chartVersion.ContentURL) == 0 {
|
|
||||||
return nil, errors.Errorf("")
|
|
||||||
}
|
|
||||||
return a.download(chartVersion.ContentURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *adapter) download(contentURL string) (io.ReadCloser, error) {
|
func (a *adapter) download(contentURL string) (io.ReadCloser, error) {
|
||||||
|
@ -24,101 +24,6 @@ func newClient(registry *model.Registry) *Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// searchPackages query the artifact package list from artifact hub.
|
|
||||||
func (c *Client) searchPackages(kind, offset, limit int, queryString string) (*PackageResponse, error) {
|
|
||||||
request, err := http.NewRequest(http.MethodGet, baseURL+searchPackages(kind, offset, limit, queryString), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.do(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
msg := &Message{}
|
|
||||||
err = json.Unmarshal(body, msg)
|
|
||||||
if err != nil {
|
|
||||||
msg.Message = string(body)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("search package list error %d: %s", resp.StatusCode, msg.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
packageResp := &PackageResponse{}
|
|
||||||
err = json.Unmarshal(body, packageResp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unmarshal package list response error: %v", err)
|
|
||||||
}
|
|
||||||
return packageResp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getAllPackages gets all of the specific kind of artifact packages from artifact hub.
|
|
||||||
func (c *Client) getAllPackages(kind int) (pkgs []*Package, err error) {
|
|
||||||
offset := 0
|
|
||||||
limit := 50
|
|
||||||
shouldContinue := true
|
|
||||||
// todo: rate limit
|
|
||||||
for shouldContinue {
|
|
||||||
pkgResp, err := c.searchPackages(HelmChart, offset, limit, "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pkgs = append(pkgs, pkgResp.Data.Packages...)
|
|
||||||
total := pkgResp.Metadata.Total
|
|
||||||
offset = offset + limit
|
|
||||||
if offset >= total {
|
|
||||||
shouldContinue = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pkgs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getHelmPackageDetail get the chart detail of a helm chart from artifact hub.
|
|
||||||
func (c *Client) getHelmPackageDetail(fullName string) (*PackageDetail, error) {
|
|
||||||
request, err := http.NewRequest(http.MethodGet, baseURL+getHelmPackageDetail(fullName), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.do(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
|
||||||
return nil, ErrHTTPNotFound
|
|
||||||
} else if resp.StatusCode != http.StatusOK {
|
|
||||||
msg := &Message{}
|
|
||||||
err = json.Unmarshal(body, msg)
|
|
||||||
if err != nil {
|
|
||||||
msg.Message = string(body)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("fetch package detail error %d: %s", resp.StatusCode, msg.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
pkgDetail := &PackageDetail{}
|
|
||||||
err = json.Unmarshal(body, pkgDetail)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unmarshal package detail response error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pkgDetail, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getHelmVersion get the package version of a helm chart from artifact hub.
|
// getHelmVersion get the package version of a helm chart from artifact hub.
|
||||||
func (c *Client) getHelmChartVersion(fullName, version string) (*ChartVersion, error) {
|
func (c *Client) getHelmChartVersion(fullName, version string) (*ChartVersion, error) {
|
||||||
request, err := http.NewRequest(http.MethodGet, baseURL+getHelmVersion(fullName, version), nil)
|
request, err := http.NewRequest(http.MethodGet, baseURL+getHelmVersion(fullName, version), nil)
|
||||||
@ -157,6 +62,43 @@ func (c *Client) getHelmChartVersion(fullName, version string) (*ChartVersion, e
|
|||||||
return chartVersion, nil
|
return chartVersion, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getReplicationInfo gets the brief info of all helm chart from artifact hub.
|
||||||
|
// see https://github.com/artifacthub/hub/issues/997
|
||||||
|
func (c *Client) getReplicationInfo() ([]*ChartInfo, error) {
|
||||||
|
request, err := http.NewRequest(http.MethodGet, baseURL+getReplicationInfo, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
msg := &Message{}
|
||||||
|
err = json.Unmarshal(body, msg)
|
||||||
|
if err != nil {
|
||||||
|
msg.Message = string(body)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("get chart replication info error %d: %s", resp.StatusCode, msg.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
var chartInfo []*ChartInfo
|
||||||
|
err = json.Unmarshal(body, &chartInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal chart replication info error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chartInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) checkHealthy() error {
|
func (c *Client) checkHealthy() error {
|
||||||
request, err := http.NewRequest(http.MethodGet, baseURL, nil)
|
request, err := http.NewRequest(http.MethodGet, baseURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -6,7 +6,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
baseURL = "https://artifacthub.io"
|
baseURL = "https://artifacthub.io"
|
||||||
|
getReplicationInfo = "/api/v1/harborReplication"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -13,9 +13,10 @@ type PackageData struct {
|
|||||||
|
|
||||||
// Package ...
|
// Package ...
|
||||||
type Package struct {
|
type Package struct {
|
||||||
PackageID string `json:"package_id"`
|
PackageID string `json:"package_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Repository *Repository `json:"repository"`
|
NormalizedName string `json:"normalized_name"`
|
||||||
|
Repository *Repository `json:"repository"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repository ...
|
// Repository ...
|
||||||
@ -29,6 +30,7 @@ type Repository struct {
|
|||||||
type PackageDetail struct {
|
type PackageDetail struct {
|
||||||
PackageID string `json:"package_id"`
|
PackageID string `json:"package_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
NormalizedName string `json:"normalized_name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
AppVersion string `json:"app_version"`
|
AppVersion string `json:"app_version"`
|
||||||
Repository RepositoryDetail `json:"repository"`
|
Repository RepositoryDetail `json:"repository"`
|
||||||
@ -70,3 +72,11 @@ type Metadata struct {
|
|||||||
type Message struct {
|
type Message struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChartInfo ...
|
||||||
|
type ChartInfo struct {
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
Package string `json:"package"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
ContentURL string `json:"url"`
|
||||||
|
}
|
||||||
|
@ -144,7 +144,7 @@ func (a *Adapter) getChartInfo(name, version string) (*chartVersionDetail, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DownloadChart downloads the specific chart
|
// DownloadChart downloads the specific chart
|
||||||
func (a *Adapter) DownloadChart(name, version string) (io.ReadCloser, error) {
|
func (a *Adapter) DownloadChart(name, version, contentURL string) (io.ReadCloser, error) {
|
||||||
info, err := a.getChartInfo(name, version)
|
info, err := a.getChartInfo(name, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -149,7 +149,7 @@ func TestDownloadChart(t *testing.T) {
|
|||||||
}
|
}
|
||||||
adapter, err := New(registry)
|
adapter, err := New(registry)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
_, err = adapter.DownloadChart("library/harbor", "1.0")
|
_, err = adapter.DownloadChart("library/harbor", "1.0", "")
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ func (a *adapter) ChartExist(name, version string) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *adapter) DownloadChart(name, version string) (io.ReadCloser, error) {
|
func (a *adapter) DownloadChart(name, version, contentURL string) (io.ReadCloser, error) {
|
||||||
versionList, err := a.client.fetchChartDetail(name)
|
versionList, err := a.client.fetchChartDetail(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -146,7 +146,7 @@ func (a *adapter) getChartInfo(name, version string) (info *tcrChartVersionDetai
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *adapter) DownloadChart(name, version string) (rc io.ReadCloser, err error) {
|
func (a *adapter) DownloadChart(name, version, contentURL string) (rc io.ReadCloser, err error) {
|
||||||
var info *tcrChartVersionDetail
|
var info *tcrChartVersionDetail
|
||||||
info, err = a.getChartInfo(name, version)
|
info, err = a.getChartInfo(name, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -37,8 +37,9 @@ func factory(logger trans.Logger, stopFunc trans.StopFunc) (trans.Transfer, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
type chart struct {
|
type chart struct {
|
||||||
name string
|
name string
|
||||||
version string
|
version string
|
||||||
|
contentURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
type transfer struct {
|
type transfer struct {
|
||||||
@ -62,9 +63,15 @@ func (t *transfer) Transfer(src *model.Resource, dst *model.Resource) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var contentURL string
|
||||||
|
if len(src.ExtendedInfo) > 0 {
|
||||||
|
contentURL = src.ExtendedInfo["contentURL"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
srcChart := &chart{
|
srcChart := &chart{
|
||||||
name: src.Metadata.Repository.Name,
|
name: src.Metadata.Repository.Name,
|
||||||
version: src.Metadata.Artifacts[0].Tags[0],
|
version: src.Metadata.Artifacts[0].Tags[0],
|
||||||
|
contentURL: contentURL,
|
||||||
}
|
}
|
||||||
dstChart := &chart{
|
dstChart := &chart{
|
||||||
name: dst.Metadata.Repository.Name,
|
name: dst.Metadata.Repository.Name,
|
||||||
@ -151,7 +158,7 @@ func (t *transfer) copy(src, dst *chart, override bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// copy the chart between the source and destination registries
|
// copy the chart between the source and destination registries
|
||||||
chart, err := t.src.DownloadChart(src.name, src.version)
|
chart, err := t.src.DownloadChart(src.name, src.version, src.contentURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.logger.Errorf("failed to download the chart %s:%s: %v", src.name, src.version, err)
|
t.logger.Errorf("failed to download the chart %s:%s: %v", src.name, src.version, err)
|
||||||
return err
|
return err
|
||||||
|
@ -45,7 +45,7 @@ func (f *fakeRegistry) FetchCharts(filters []*model.Filter) ([]*model.Resource,
|
|||||||
func (f *fakeRegistry) ChartExist(name, version string) (bool, error) {
|
func (f *fakeRegistry) ChartExist(name, version string) (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
func (f *fakeRegistry) DownloadChart(name, version string) (io.ReadCloser, error) {
|
func (f *fakeRegistry) DownloadChart(name, version, contentURL string) (io.ReadCloser, error) {
|
||||||
r := ioutil.NopCloser(bytes.NewReader([]byte{'a'}))
|
r := ioutil.NopCloser(bytes.NewReader([]byte{'a'}))
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user