Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for OCI tarball url #8184

Merged
merged 12 commits into from
Dec 24, 2024
99 changes: 92 additions & 7 deletions cmd/asset-syncer/server/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,17 +300,102 @@ func (r *HelmRepo) Charts(ctx context.Context, fetchLatestOnly bool, chartResult
// FetchFiles retrieves the important files of a chart and version from the repo
func (r *HelmRepo) FetchFiles(cv models.ChartVersion, userAgent string, passCredentials bool) (map[string]string, error) {
authorizationHeader := ""
chartTarballURL := chartTarballURL(r.AppRepositoryInternal, cv)
chartTarballURL, err := url.Parse(chartTarballURL(r.AppRepositoryInternal, cv))
if err != nil {
return nil, err
}

if passCredentials || len(r.AuthorizationHeader) > 0 && isURLDomainEqual(chartTarballURL, r.URL) {
if passCredentials || len(r.AuthorizationHeader) > 0 && isURLDomainEqual(chartTarballURL.String(), r.URL) {
authorizationHeader = r.AuthorizationHeader
}

return tarutil.FetchChartDetailFromTarballUrl(
chartTarballURL,
userAgent,
authorizationHeader,
r.netClient)
// If URL points to an OCI chart, we transform its URL to its tgz blob URL
if chartTarballURL.Scheme == "oci" {
return FetchChartDetailFromOciUrl(chartTarballURL, userAgent, authorizationHeader, r.netClient)
} else {
return FetchChartDetailFromTarballUrl(chartTarballURL, userAgent, authorizationHeader, r.netClient)
}
}

// Fetches helm chart details from a gzipped tarball
//
// name is expected in format "foo/bar" or "foo%2Fbar" if url-escaped
func FetchChartDetailFromTarballUrl(chartTarballURL *url.URL, userAgent string, authz string, netClient *http.Client) (map[string]string, error) {
reqHeaders := make(map[string]string)
if len(userAgent) > 0 {
reqHeaders["User-Agent"] = userAgent
}
if len(authz) > 0 {
reqHeaders["Authorization"] = authz
}

// use our "standard" http-client library
reader, _, err := httpclient.GetStream(chartTarballURL.String(), netClient, reqHeaders)
if reader != nil {
defer reader.Close()
}

if err != nil {
return nil, err
}
return tarutil.FetchChartDetailFromTarball(reader)
}

func FetchChartDetailFromOciUrl(chartTarballURL *url.URL, userAgent string, authorizationHeader string, netClient *http.Client) (map[string]string, error) {
migruiz4 marked this conversation as resolved.
Show resolved Hide resolved
// If URL points to an OCI chart, we transform its URL to its tgz blob URL
// Extract the tag from the chart Path
chartTag := "latest"
i := strings.Index(chartTarballURL.Path, ":")
if i >= 0 {
chartTag = chartTarballURL.Path[i+1:]
chartTarballURL.Path = chartTarballURL.Path[:i]
}
// Separate the appname from the oci Url
j := strings.LastIndex(chartTarballURL.Path, "/")
appName := chartTarballURL.Path[j+1:]
chartTarballURL.Path = chartTarballURL.Path[:j]
// TODO: I would like to refactor the OciAPIClient to be generic and allow generating an OCI client without all the oci-catalog specific code
// IMPORTANT: Currently, getOrasRepoClient(appname, userAgent) is too specific, I would need to be able to generate an OCI client for other general purposes by simply providing an url.
o := OciAPIClient{RegistryNamespaceUrl: chartTarballURL, HttpClient: netClient, GrpcClient: nil}
orasRepoClient, err := o.getOrasRepoClient(appName, "")
if err != nil {
panic(err)
migruiz4 marked this conversation as resolved.
Show resolved Hide resolved
}
ctx := context.TODO()
// TODO: This code is too similar to the function 'IsHelmChart'.
// Again, I would like to move both functions into a common 'fetchOciManifest' function in order to simplify the code structure
manifestDescriptor, rc, err := orasRepoClient.Manifests().FetchReference(ctx, chartTag)
if err != nil {
panic(err)
}
defer rc.Close()

manifestData, err := content.ReadAll(rc, manifestDescriptor)
if err != nil {
panic(err)
}

var manifest OCIManifest
err = json.Unmarshal(manifestData, &manifest)

if len(manifest.Layers) != 1 || manifest.Layers[0].MediaType != "application/vnd.cncf.helm.chart.content.v1.tar+gzip" {
log.Errorf("Unexpected layer in index manifest: %v", manifest)
return nil, fmt.Errorf("unexpected layer in chart manifest")
}

blobDescriptor, err := orasRepoClient.Blobs().Resolve(ctx, manifest.Layers[0].Digest)
if err != nil {
return nil, err
}
reader, err := orasRepoClient.Blobs().Fetch(ctx, blobDescriptor)
if reader != nil {
defer reader.Close()
}
if err != nil {
return nil, err
}

return tarutil.FetchChartDetailFromTarball(reader)
}

// TagList represents a list of tags as specified at
Expand Down
2 changes: 1 addition & 1 deletion cmd/kubeapps-apis/plugins/helm/packages/v1alpha1/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -963,7 +963,7 @@ func (s *Server) fetchChartWithRegistrySecrets(ctx context.Context, headers http
},
appRepo,
caCertSecret, authSecret,
s.chartClientFactory.New(appRepo.Spec.Type, userAgentString),
s.chartClientFactory.New(tarballURL, userAgentString),
)
if err != nil {
return nil, nil, connect.NewError(connect.CodeInternal, fmt.Errorf("Unable to fetch the chart %s from the namespace %q: %w", chartDetails.ChartName, appRepo.Namespace, err))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,16 +184,12 @@ func (c *OCIRepoClient) GetChart(details *ChartDetails, repoURL string) (*chart.
if c.puller == nil {
return nil, fmt.Errorf("unable to retrieve chart, Init should be called first")
}
parsedURL, err := url.ParseRequestURI(strings.TrimSpace(repoURL))
if err != nil {
return nil, err
}
unescapedChartName, err := url.QueryUnescape(details.ChartName)
parsedURL, err := url.Parse(strings.TrimSpace(details.TarballURL))
if err != nil {
return nil, err
}

ref := path.Join(parsedURL.Host, parsedURL.Path, fmt.Sprintf("%s:%s", unescapedChartName, details.Version))
ref := path.Join(parsedURL.Host, parsedURL.Path)
chartBuffer, _, err := c.puller.PullOCIChart(ref)
if err != nil {
return nil, err
Expand All @@ -215,12 +211,11 @@ type ChartClientFactoryInterface interface {
type ChartClientFactory struct{}

// New for ClientResolver
func (c *ChartClientFactory) New(repoType, userAgent string) ChartClient {
func (c *ChartClientFactory) New(tarballUrl string, userAgent string) ChartClient {
var client ChartClient
switch repoType {
case "oci":
if strings.HasPrefix(tarballUrl, "oci://") {
client = NewOCIClient(userAgent)
default:
} else {
client = NewChartClient(userAgent)
}
return client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ func (f *ChartClient) Init(appRepo *appRepov1.AppRepository, caCertSecret *corev
type ChartClientFactory struct{}

// New returns a fake ChartClient
func (c *ChartClientFactory) New(repoType, userAgent string) utils.ChartClient {
func (c *ChartClientFactory) New(tarballURL string, userAgent string) utils.ChartClient {
return &ChartClient{}
}
27 changes: 0 additions & 27 deletions pkg/tarutil/tarutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,13 @@ import (
"bytes"
"compress/gzip"
"io"
"net/http"
"path"
"regexp"
"strings"

chart "github.com/vmware-tanzu/kubeapps/pkg/chart/models"
httpclient "github.com/vmware-tanzu/kubeapps/pkg/http-client"
)

// Fetches helm chart details from a gzipped tarball
//
// name is expected in format "foo/bar" or "foo%2Fbar" if url-escaped
func FetchChartDetailFromTarballUrl(chartTarballURL string, userAgent string, authz string, netClient *http.Client) (map[string]string, error) {
reqHeaders := make(map[string]string)
if len(userAgent) > 0 {
reqHeaders["User-Agent"] = userAgent
}
if len(authz) > 0 {
reqHeaders["Authorization"] = authz
}

// use our "standard" http-client library
reader, _, err := httpclient.GetStream(chartTarballURL, netClient, reqHeaders)
if reader != nil {
defer reader.Close()
}

if err != nil {
return nil, err
}

return FetchChartDetailFromTarball(reader)
}

// Fetches helm chart details from a gzipped tarball
//
// name is expected in format "foo/bar" or "foo%2Fbar" if url-escaped
Expand Down
Loading