Skip to content

Commit

Permalink
publish npm packages (#636)
Browse files Browse the repository at this point in the history
* provide blob uploader for NPM packages.
  based on gist from:
  https://gist.github.com/cloverstd/7355e95424d59256123a1093f76f78a6
  • Loading branch information
hilmarf authored Feb 9, 2024
1 parent 1004c32 commit 1e39e53
Show file tree
Hide file tree
Showing 12 changed files with 536 additions and 25 deletions.
5 changes: 1 addition & 4 deletions pkg/contexts/credentials/cpi/const.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors.
//
// SPDX-License-Identifier: Apache-2.0

package cpi

import (
Expand All @@ -13,6 +9,7 @@ const (

ATTR_TYPE = internal.ATTR_TYPE
ATTR_USERNAME = internal.ATTR_USERNAME
ATTR_EMAIL = internal.ATTR_EMAIL
ATTR_PASSWORD = internal.ATTR_PASSWORD
ATTR_SERVER_ADDRESS = internal.ATTR_SERVER_ADDRESS
ATTR_TOKEN = internal.ATTR_TOKEN
Expand Down
5 changes: 1 addition & 4 deletions pkg/contexts/credentials/internal/const.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors.
//
// SPDX-License-Identifier: Apache-2.0

package internal

const (
ID_TYPE = "type"

ATTR_TYPE = "type"
ATTR_USERNAME = "username"
ATTR_EMAIL = "email"
ATTR_PASSWORD = "password"
ATTR_CERTIFICATE_AUTHORITY = "certificateAuthority"
ATTR_CERTIFICATE = "certificate"
Expand Down
8 changes: 2 additions & 6 deletions pkg/contexts/ocm/accessmethods/npm/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# `npm` - NPM packages in an NPM registry

# `npm` - NPM packages in a NPM registry (e.g. npmjs.com)

### Synopsis
```
Expand All @@ -12,7 +11,6 @@ Provided blobs use the following media type: `application/x-tgz`

This method implements the access of an NPM package from an NPM registry.


### Specification Versions

Supported specification version is `v1`
Expand All @@ -31,6 +29,4 @@ The type specific specification fields are:

- **`version`** *string*

The version name of the NPM package.


The version of the NPM package.
10 changes: 3 additions & 7 deletions pkg/contexts/ocm/accessmethods/npm/method.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@ import (
"github.com/open-component-model/ocm/pkg/runtime"
)

// TODO: open questions
// - authentication???
// - writing packages

// Type is the access type of NPM registry.
const (
Type = "npm"
Expand Down Expand Up @@ -67,19 +63,19 @@ func New(registry, pkg, version string) *AccessSpec {
}
}

func (a *AccessSpec) Describe(ctx accspeccpi.Context) string {
func (a *AccessSpec) Describe(_ accspeccpi.Context) string {
return fmt.Sprintf("NPM package %s:%s in registry %s", a.Package, a.Version, a.Registry)
}

func (_ *AccessSpec) IsLocal(accspeccpi.Context) bool {
return false
}

func (a *AccessSpec) GlobalAccessSpec(ctx accspeccpi.Context) accspeccpi.AccessSpec {
func (a *AccessSpec) GlobalAccessSpec(_ accspeccpi.Context) accspeccpi.AccessSpec {
return a
}

func (a *AccessSpec) GetReferenceHint(cv accspeccpi.ComponentVersionAccess) string {
func (a *AccessSpec) GetReferenceHint(_ accspeccpi.ComponentVersionAccess) string {
return a.Package + ":" + a.Version
}

Expand Down
133 changes: 133 additions & 0 deletions pkg/contexts/ocm/blobhandler/handlers/generic/npm/blobhandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package npm

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"

"github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/npm"
"github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
"github.com/open-component-model/ocm/pkg/logging"
"github.com/open-component-model/ocm/pkg/mime"
)

type artifactHandler struct {
spec *Config
}

func NewArtifactHandler(repospec *Config) cpi.BlobHandler {
return &artifactHandler{repospec}
}

func (b *artifactHandler) StoreBlob(blob cpi.BlobAccess, _ string, _ string, _ cpi.AccessSpec, ctx cpi.StorageContext) (cpi.AccessSpec, error) {
if b.spec == nil {
return nil, nil
}

mimeType := blob.MimeType()
if mime.MIME_TGZ != mimeType && mime.MIME_TGZ_ALT != mimeType {
return nil, nil
}

if b.spec.Url == "" {
return nil, fmt.Errorf("NPM registry url not provided")
}

blobReader, err := blob.Reader()
if err != nil {
return nil, err
}
defer blobReader.Close()

data, err := io.ReadAll(blobReader)
if err != nil {
return nil, err
}

// read package.json from tarball to get name, version, etc.
log := logging.Context().Logger(REALM)
log.Debug("reading package.json from tarball")
var pkg *Package
pkg, err = prepare(data)
if err != nil {
return nil, err
}
tbName := pkg.Name + "-" + pkg.Version + ".tgz"
pkg.Dist.Tarball = b.spec.Url + pkg.Name + "/-/" + tbName
log = log.WithValues("package", pkg.Name, "version", pkg.Version)
log.Debug("identified")

// use user+pass+mail from credentials to login and retrieve bearer token
cred := GetCredentials(ctx.GetContext(), b.spec.Url, pkg.Name)
username := cred[ATTR_USERNAME]
password := cred[ATTR_PASSWORD]
email := cred[ATTR_EMAIL]
if username == "" || password == "" || email == "" {
return nil, fmt.Errorf("username, password or email missing")
}
log = log.WithValues("user", username, "repo", b.spec.Url)
log.Debug("login")
token, err := login(b.spec.Url, username, password, email)
if err != nil {
return nil, err
}

// check if package exists
exists, err := packageExists(b.spec.Url, *pkg, token)
if err != nil {
return nil, err
}
if exists {
log.Debug("package+version already exists, skipping upload")
return npm.New(b.spec.Url, pkg.Name, pkg.Version), nil
}

// prepare body for upload
body := Body{
ID: pkg.Name,
Name: pkg.Name,
Description: pkg.Description,
}
body.Versions = map[string]*Package{
pkg.Version: pkg,
}
body.DistTags.Latest = pkg.Version
body.Readme = pkg.Readme
body.Attachments = map[string]*Attachment{
tbName: NewAttachment(data),
}
marshal, err := json.Marshal(body)
if err != nil {
return nil, err
}

// prepare PUT request
req, err := http.NewRequestWithContext(context.Background(), http.MethodPut, b.spec.Url+"/"+url.PathEscape(pkg.Name), bytes.NewReader(marshal))
if err != nil {
return nil, err
}
req.Header.Set("authorization", "Bearer "+token)
req.Header.Set("content-type", "application/json")

// send PUT request - upload tgz
client := http.Client{}
log.Debug("uploading")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
all, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("http (%d) - failed to upload package: %s", resp.StatusCode, string(all))
}
log.Debug("successfully uploaded")
return npm.New(b.spec.Url, pkg.Name, pkg.Version), nil
}
24 changes: 24 additions & 0 deletions pkg/contexts/ocm/blobhandler/handlers/generic/npm/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package npm_test

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/open-component-model/ocm/pkg/contexts/ocm/blobhandler/handlers/generic/npm"
"github.com/open-component-model/ocm/pkg/registrations"
. "github.com/open-component-model/ocm/pkg/testutils"
)

var _ = Describe("Config deserialization Test Environment", func() {

It("deserializes string", func() {
cfg := Must(registrations.DecodeConfig[npm.Config]("test"))
Expect(cfg).To(Equal(&npm.Config{"test"}))
})

It("deserializes struct", func() {
cfg := Must(registrations.DecodeConfig[npm.Config](`{"Url":"test"}`))
Expect(cfg).To(Equal(&npm.Config{"test"}))
})

})
22 changes: 22 additions & 0 deletions pkg/contexts/ocm/blobhandler/handlers/generic/npm/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package npm

import (
"github.com/open-component-model/ocm/pkg/contexts/credentials/cpi"
"github.com/open-component-model/ocm/pkg/logging"
)

const (
// CONSUMER_TYPE is the npm repository type.
CONSUMER_TYPE = "Registry.npmjs.com"
BLOB_HANDLER_NAME = "ocm/npmPackage"

// ATTR_USERNAME is the username attribute. Required for login at any npm registry.
ATTR_USERNAME = cpi.ATTR_USERNAME
// ATTR_PASSWORD is the password attribute. Required for login at any npm registry.
ATTR_PASSWORD = cpi.ATTR_PASSWORD
// ATTR_EMAIL is the email attribute. Required for login at any npm registry.
ATTR_EMAIL = cpi.ATTR_EMAIL
)

// Logging Realm.
var REALM = logging.DefineSubRealm("NPM registry", "NPM")
48 changes: 48 additions & 0 deletions pkg/contexts/ocm/blobhandler/handlers/generic/npm/identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package npm

import (
"path"

. "net/url"

"github.com/open-component-model/ocm/pkg/common"
"github.com/open-component-model/ocm/pkg/contexts/credentials/cpi"
"github.com/open-component-model/ocm/pkg/contexts/credentials/identity/hostpath"
"github.com/open-component-model/ocm/pkg/listformat"
)

func init() {
attrs := listformat.FormatListElements("", listformat.StringElementDescriptionList{
ATTR_USERNAME, "the basic auth user name",
ATTR_PASSWORD, "the basic auth password",
ATTR_EMAIL, "NPM registry, require an email address",
})

cpi.RegisterStandardIdentity(CONSUMER_TYPE, hostpath.IdentityMatcher(CONSUMER_TYPE), `NPM repository
It matches the <code>`+CONSUMER_TYPE+`</code> consumer type and additionally acts like
the <code>`+hostpath.IDENTITY_TYPE+`</code> type.`,
attrs)
}

func GetConsumerId(rawURL string, pkgName string) cpi.ConsumerIdentity {
url, err := Parse(rawURL)
if err != nil {
return nil
}

url.Path = path.Join(url.Path, pkgName)
return hostpath.GetConsumerIdentity(CONSUMER_TYPE, url.String())
}

func GetCredentials(ctx cpi.ContextProvider, repoUrl string, pkgName string) common.Properties {
id := GetConsumerId(repoUrl, pkgName)
if id == nil {
return nil
}
credentials, err := cpi.CredentialsForConsumer(ctx.CredentialsContext(), id)
if credentials == nil || err != nil {
return nil
}
return credentials.Properties()
}
Loading

0 comments on commit 1e39e53

Please sign in to comment.