diff --git a/api/ocm/extensions/blobhandler/handlers/generic/plugin/testdata/test b/api/ocm/extensions/blobhandler/handlers/generic/plugin/testdata/test index 7b08e5d838..87e3556cc3 100755 --- a/api/ocm/extensions/blobhandler/handlers/generic/plugin/testdata/test +++ b/api/ocm/extensions/blobhandler/handlers/generic/plugin/testdata/test @@ -13,6 +13,7 @@ media= artifact= hint= creds= +digest= args=( ) parseArgs() { @@ -21,6 +22,9 @@ parseArgs() { --mediaType|-m) media="$2" shift 2;; + --digest|-d) + digest="$2" + shift 2;; --artifactType|-a) artifact="$2" shift 2;; diff --git a/cmds/jfrogplugin/uploaders/helm/convert.go b/cmds/jfrogplugin/uploaders/helm/convert.go new file mode 100644 index 0000000000..ab9e5fbe81 --- /dev/null +++ b/cmds/jfrogplugin/uploaders/helm/convert.go @@ -0,0 +1,72 @@ +package helm + +import ( + "errors" + "fmt" + "io" + + "ocm.software/ocm/api/oci" + "ocm.software/ocm/api/oci/extensions/repositories/artifactset" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" +) + +func ConvertArtifactSetHelmChartToPlainTGZChart(reader io.Reader) (_ io.ReadCloser, _ string, err error) { + set, err := artifactset.Open(accessobj.ACC_READONLY, "", 0, accessio.Reader(io.NopCloser(reader))) + if err != nil { + return nil, "", fmt.Errorf("failed to open helm OCI artifact as artifact set: %w", err) + } + defer func() { + err = errors.Join(err, set.Close()) + }() + + art, err := set.GetArtifact(set.GetMain().String()) + if err != nil { + return nil, "", fmt.Errorf("failed to get artifact from set: %w", err) + } + defer func() { + err = errors.Join(err, art.Close()) + }() + + chartTgz, provenance, err := accessSingleLayerOCIHelmChart(art) + if err != nil { + return nil, "", fmt.Errorf("failed to access OCI artifact as a single layer helm OCI image: %w", err) + } + defer func() { + err = errors.Join(err, chartTgz.Close()) + if provenance != nil { + err = errors.Join(err, provenance.Close()) + } + }() + + chartReader, err := chartTgz.Reader() + if err != nil { + return nil, "", fmt.Errorf("failed to get reader for chart tgz: %w", err) + } + + digest := chartTgz.Digest().String() + + return chartReader, digest, nil +} + +func accessSingleLayerOCIHelmChart(art oci.ArtifactAccess) (chart oci.BlobAccess, prov oci.BlobAccess, err error) { + m := art.ManifestAccess() + if m == nil { + return nil, nil, errors.New("artifact is no image manifest") + } + if len(m.GetDescriptor().Layers) < 1 { + return nil, nil, errors.New("no layers found") + } + + if chart, err = m.GetBlob(m.GetDescriptor().Layers[0].Digest); err != nil { + return nil, nil, err + } + + if len(m.GetDescriptor().Layers) > 1 { + if prov, err = m.GetBlob(m.GetDescriptor().Layers[1].Digest); err != nil { + return nil, nil, err + } + } + + return chart, prov, nil +} diff --git a/cmds/jfrogplugin/uploaders/helm/helm.go b/cmds/jfrogplugin/uploaders/helm/helm.go index 07861fb5ae..a2d098c55b 100644 --- a/cmds/jfrogplugin/uploaders/helm/helm.go +++ b/cmds/jfrogplugin/uploaders/helm/helm.go @@ -1,6 +1,7 @@ package helm import ( + "bytes" "context" "fmt" "io" @@ -15,8 +16,12 @@ import ( "ocm.software/ocm/api/credentials" "ocm.software/ocm/api/credentials/cpi" "ocm.software/ocm/api/credentials/identity/hostpath" + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/oci/extensions/repositories/artifactset" "ocm.software/ocm/api/ocm/extensions/artifacttypes" "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/tech/helm" + "ocm.software/ocm/api/tech/helm/loader" "ocm.software/ocm/api/utils/runtime" ) @@ -122,7 +127,7 @@ func (a *Uploader) ValidateSpecification(_ ppi.Plugin, spec ppi.UploadTargetSpec // 3. creating a request respecting the passed credentials based on SetHeadersFromCredentials // 4. uploading the passed blob as is (expected to be a tgz byte stream) // 5. intepreting the JFrog API response, and converting it from ArtifactoryUploadResponse to ppi.AccessSpec -func (a *Uploader) Upload(_ ppi.Plugin, arttype, _, hint, digest string, targetSpec ppi.UploadTargetSpec, creds credentials.Credentials, reader io.Reader) (ppi.AccessSpecProvider, error) { +func (a *Uploader) Upload(_ ppi.Plugin, arttype, mediaType, hint, digest string, targetSpec ppi.UploadTargetSpec, creds credentials.Credentials, reader io.Reader) (ppi.AccessSpecProvider, error) { if arttype != artifacttypes.HELM_CHART { return nil, fmt.Errorf("unsupported artifact type %s", arttype) } @@ -132,6 +137,28 @@ func (a *Uploader) Upload(_ ppi.Plugin, arttype, _, hint, digest string, targetS return nil, fmt.Errorf("the type %T is not a valid target spec type", spec) } + switch mediaType { + case helm.ChartMediaType: + // if it is a native chart tgz we can pass it on as is + var buf bytes.Buffer + chart, err := loader.LoadArchive(io.TeeReader(reader, &buf)) + if err != nil { + return nil, fmt.Errorf("failed to load chart: %w", err) + } + spec.Name = chart.Metadata.Name + spec.Version = chart.Metadata.Version + reader = &buf + case artifactset.MediaType(artdesc.MediaTypeImageManifest): + // if we have an artifact set (ocm custom version of index + locally colocated blobs as files, we need + // to translate it. + var err error + if reader, digest, err = ConvertArtifactSetHelmChartToPlainTGZChart(reader); err != nil { + return nil, fmt.Errorf("failed to convert OCI Helm Chart to plain TGZ: %w", err) + } + default: + return nil, fmt.Errorf("unsupported media type %s", mediaType) + } + if err := EnsureSpecWithHelpFromHint(spec, hint); err != nil { return nil, fmt.Errorf("could not ensure spec to be ready for upload: %w", err) } @@ -149,6 +176,10 @@ func (a *Uploader) Upload(_ ppi.Plugin, arttype, _, hint, digest string, targetS return nil, fmt.Errorf("failed to upload: %w", err) } + if err := ReindexChart(ctx, a.Client, spec.URL, spec.Repository, creds); err != nil { + return nil, fmt.Errorf("failed to reindex chart: %w", err) + } + return func() ppi.AccessSpec { return access }, nil diff --git a/cmds/jfrogplugin/uploaders/helm/reindex.go b/cmds/jfrogplugin/uploaders/helm/reindex.go new file mode 100644 index 0000000000..7e8f34241e --- /dev/null +++ b/cmds/jfrogplugin/uploaders/helm/reindex.go @@ -0,0 +1,57 @@ +package helm + +import ( + "fmt" + "io" + "net/http" + "path" + + "golang.org/x/net/context" + + "ocm.software/ocm/api/credentials" +) + +func ReindexChart(ctx context.Context, client *http.Client, artifactoryURL string, + repository string, + creds credentials.Credentials) (err error) { + reindexURL, err := convertToReindexURL(artifactoryURL, repository) + if err != nil { + return fmt.Errorf("failed to convert to reindex URL: %w", err) + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, reindexURL, nil) + if err != nil { + return fmt.Errorf("failed to create reindex request: %w", err) + } + SetHeadersFromCredentials(req, creds) + req.Header = req.Header.Clone() + + res, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to reindex chart: %w", err) + } + + if res.StatusCode != http.StatusOK { + responseBytes, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("failed to read response body but server returned %v: %w", res.StatusCode, err) + } + var body string + if len(responseBytes) > 0 { + body = fmt.Sprintf(": %s", string(responseBytes)) + } + return fmt.Errorf("invalid response (status %v) while reindexing at %q: %s", res.StatusCode, reindexURL, body) + } + + return nil +} + +// convertToReindexURL converts the base URL and repository to a reindex URL. +// see https://jfrog.com/help/r/jfrog-rest-apis/calculate-helm-chart-index for the reindex API +func convertToReindexURL(baseURL string, repository string) (string, error) { + u, err := parseURLAllowNoScheme(baseURL) + if err != nil { + return "", fmt.Errorf("failed to parse url: %w", err) + } + u.Path = path.Join(u.Path, "api", "helm", repository, "reindex") + return u.String(), nil +} diff --git a/cmds/jfrogplugin/uploaders/helm/upload_test.go b/cmds/jfrogplugin/uploaders/helm/upload_test.go index 8d11feaccb..e5737d16fb 100644 --- a/cmds/jfrogplugin/uploaders/helm/upload_test.go +++ b/cmds/jfrogplugin/uploaders/helm/upload_test.go @@ -53,7 +53,7 @@ func TestUpload(t *testing.T) { accessSpec, err := Upload(ctx, data, client, url, credentials.DirectCredentials{ credentials.ATTR_USERNAME: "foo", credentials.ATTR_PASSWORD: "bar", - }) + }, "") if err != nil { t.Fatalf("unexpected error: %v", err) diff --git a/docs/pluginreference/plugin_upload_put.md b/docs/pluginreference/plugin_upload_put.md index a0bf9df648..6c5723b2b0 100644 --- a/docs/pluginreference/plugin_upload_put.md +++ b/docs/pluginreference/plugin_upload_put.md @@ -12,6 +12,7 @@ plugin upload put [] [] -a, --artifactType string artifact type of input blob -C, --credential = dedicated credential value (default []) -c, --credentials YAML credentials + -d, --digest string digest of the blob -h, --help help for put -H, --hint string reference hint for storing blob -m, --mediaType string media type of input blob