228 lines
5.3 KiB
Go
228 lines
5.3 KiB
Go
/*
|
|
* Copyright 2024 the KubeSphere Authors.
|
|
* Please refer to the LICENSE file in the root directory of the project.
|
|
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
|
*/
|
|
|
|
package oci
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
"strconv"
|
|
"time"
|
|
|
|
"oras.land/oras-go/pkg/registry"
|
|
"oras.land/oras-go/pkg/registry/remote"
|
|
"oras.land/oras-go/pkg/registry/remote/auth"
|
|
)
|
|
|
|
type RepositoryOptions remote.Repository
|
|
type RegistryOption func(*Registry)
|
|
|
|
// Registry is an HTTP client to a remote registry by oras-go 2.x.
|
|
// Registry with authentication requires an administrator account.
|
|
type Registry struct {
|
|
RepositoryOptions
|
|
|
|
RepositoryListPageSize int
|
|
|
|
username string
|
|
password string
|
|
timeout time.Duration
|
|
insecureSkipVerifyTLS bool
|
|
}
|
|
|
|
func NewRegistry(name string, options ...RegistryOption) (*Registry, error) {
|
|
ref := registry.Reference{
|
|
Registry: name,
|
|
}
|
|
if err := ref.ValidateRegistry(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reg := &Registry{RepositoryOptions: RepositoryOptions{
|
|
Reference: ref,
|
|
}}
|
|
for _, option := range options {
|
|
option(reg)
|
|
}
|
|
|
|
headers := http.Header{}
|
|
headers.Set("User-Agent", "kubesphere.io")
|
|
reg.Client = &auth.Client{
|
|
Client: &http.Client{
|
|
Timeout: reg.timeout,
|
|
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: reg.insecureSkipVerifyTLS}},
|
|
},
|
|
Header: headers,
|
|
Credential: func(_ context.Context, _ string) (auth.Credential, error) {
|
|
if reg.username == "" && reg.password == "" {
|
|
return auth.EmptyCredential, nil
|
|
}
|
|
|
|
return auth.Credential{
|
|
Username: reg.username,
|
|
Password: reg.password,
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
_, err := reg.IsPlainHttp()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return reg, nil
|
|
}
|
|
|
|
func WithBasicAuth(username, password string) RegistryOption {
|
|
return func(reg *Registry) {
|
|
reg.username = username
|
|
reg.password = password
|
|
}
|
|
}
|
|
|
|
func WithTimeout(timeout time.Duration) RegistryOption {
|
|
return func(reg *Registry) {
|
|
reg.timeout = timeout
|
|
}
|
|
}
|
|
|
|
func WithInsecureSkipVerifyTLS(insecureSkipVerifyTLS bool) RegistryOption {
|
|
return func(reg *Registry) {
|
|
reg.insecureSkipVerifyTLS = insecureSkipVerifyTLS
|
|
}
|
|
}
|
|
|
|
func (r *Registry) client() remote.Client {
|
|
if r.Client == nil {
|
|
return auth.DefaultClient
|
|
}
|
|
return r.Client
|
|
}
|
|
|
|
func (r *Registry) do(req *http.Request) (*http.Response, error) {
|
|
return r.client().Do(req)
|
|
}
|
|
|
|
func (r *Registry) IsPlainHttp() (bool, error) {
|
|
schemaProbeList := []bool{false, true}
|
|
|
|
var err error
|
|
for _, probe := range schemaProbeList {
|
|
r.PlainHTTP = probe
|
|
err = r.Ping(context.Background())
|
|
if err == nil {
|
|
return probe, nil
|
|
}
|
|
}
|
|
|
|
return r.PlainHTTP, err
|
|
}
|
|
|
|
func (r *Registry) Ping(ctx context.Context) error {
|
|
url := buildRegistryBaseURL(r.PlainHTTP, r.Reference)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := r.do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
switch resp.StatusCode {
|
|
case http.StatusOK:
|
|
return nil
|
|
case http.StatusNotFound:
|
|
return errors.New("not found")
|
|
default:
|
|
return ParseErrorResponse(resp)
|
|
}
|
|
}
|
|
|
|
func (r *Registry) Repositories(ctx context.Context, last string, fn func(repos []string) error) error {
|
|
url := buildRegistryCatalogURL(r.PlainHTTP, r.Reference)
|
|
var err error
|
|
for err == nil {
|
|
url, err = r.repositories(ctx, last, fn, url)
|
|
// clear `last` for subsequent pages
|
|
last = ""
|
|
}
|
|
if err != errNoLink {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Registry) repositories(ctx context.Context, last string, fn func(repos []string) error, url string) (string, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if r.RepositoryListPageSize > 0 || last != "" {
|
|
q := req.URL.Query()
|
|
if r.RepositoryListPageSize > 0 {
|
|
q.Set("n", strconv.Itoa(r.RepositoryListPageSize))
|
|
}
|
|
if last != "" {
|
|
q.Set("last", last)
|
|
}
|
|
req.URL.RawQuery = q.Encode()
|
|
}
|
|
resp, err := r.do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", ParseErrorResponse(resp)
|
|
}
|
|
var page struct {
|
|
Repositories []string `json:"repositories"`
|
|
}
|
|
lr := limitReader(resp.Body, r.MaxMetadataBytes)
|
|
if err := json.NewDecoder(lr).Decode(&page); err != nil {
|
|
return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
|
|
}
|
|
if err := fn(page.Repositories); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return parseLink(resp)
|
|
}
|
|
|
|
func (r *Registry) Repository(ctx context.Context, name string) (registry.Repository, error) {
|
|
ref := registry.Reference{
|
|
Registry: r.Reference.Registry,
|
|
Repository: name,
|
|
}
|
|
if err := ref.ValidateRepository(); err != nil {
|
|
return nil, err
|
|
}
|
|
repo := r.repository((*remote.Repository)(&r.RepositoryOptions))
|
|
repo.Reference = ref
|
|
return repo, nil
|
|
}
|
|
|
|
func (r *Registry) repository(repo *remote.Repository) *remote.Repository {
|
|
return &remote.Repository{
|
|
Client: repo.Client,
|
|
Reference: repo.Reference,
|
|
PlainHTTP: repo.PlainHTTP,
|
|
ManifestMediaTypes: slices.Clone(repo.ManifestMediaTypes),
|
|
TagListPageSize: repo.TagListPageSize,
|
|
ReferrerListPageSize: repo.ReferrerListPageSize,
|
|
MaxMetadataBytes: repo.MaxMetadataBytes,
|
|
}
|
|
}
|