diff --git a/docs/reference/ocm.md b/docs/reference/ocm.md
index 863d813065..77ee2b50e7 100644
--- a/docs/reference/ocm.md
+++ b/docs/reference/ocm.md
@@ -50,9 +50,12 @@ location a default configuration is composed according to known type specific
configuration files.
The following configuration sources are used:
- - The docker configuration file at ~/.docker/config.jaon
is
+ - The docker configuration file at ~/.docker/config.json
is
read to feed in the configured credentials for OCI registries.
+ - The npm configuration file at ~/.npmrc
is
+ read to feed in the configured credentials for NPM registries.
+
With the option --cred
it is possible to specify arbitrary credentials
diff --git a/docs/reference/ocm_credential-handling.md b/docs/reference/ocm_credential-handling.md
index 049c39a3dc..fa503e5457 100644
--- a/docs/reference/ocm_credential-handling.md
+++ b/docs/reference/ocm_credential-handling.md
@@ -170,6 +170,19 @@ The following credential consumer types are used/supported:
- certificateAuthority
: the certificate authority certificate used to verify certificates
+ - Registry.npmjs.com
: NPM repository
+
+ It matches the Registry.npmjs.com
consumer type and additionally acts like
+ the hostpath
type.
+
+ Credential consumers of the consumer type Registry.npmjs.com evaluate the following credential properties:
+
+ - username
: the basic auth user name
+ - password
: the basic auth password
+ - email
: NPM registry, require an email address
+ - token
: the token attribute. May exist after login at any npm registry. Check your .npmrc file!
+
+
- S3
: S3 credential matcher
This matcher is a hostpath matcher.
@@ -306,6 +319,21 @@ behaviours are described in the following list:
is read.
+- Credential provider NPMConfig
+
+ This repository type can be used to access credentials stored in a file
+ following the NPM npmrc format (~/.npmrc). It take into account the
+ credentials helper section, also. If enabled, the described
+ credentials will be automatically assigned to appropriate consumer ids.
+
+ The following versions are supported:
+ - Version v1
+
+ The repository specification supports the following fields:
+ - npmrcFile
: *string*: the file path to a NPM npmrc file
+ - propagateConsumerIdentity
: *bool*(optional): enable consumer id propagation
+
+
### SEE ALSO
diff --git a/docs/reference/ocm_get_credentials.md b/docs/reference/ocm_get_credentials.md
index 8d56179552..e6b09c0373 100644
--- a/docs/reference/ocm_get_credentials.md
+++ b/docs/reference/ocm_get_credentials.md
@@ -96,6 +96,19 @@ Matchers exist for the following usage contexts or consumer types:
- certificateAuthority
: the certificate authority certificate used to verify certificates
+ - Registry.npmjs.com
: NPM repository
+
+ It matches the Registry.npmjs.com
consumer type and additionally acts like
+ the hostpath
type.
+
+ Credential consumers of the consumer type Registry.npmjs.com evaluate the following credential properties:
+
+ - username
: the basic auth user name
+ - password
: the basic auth password
+ - email
: NPM registry, require an email address
+ - token
: the token attribute. May exist after login at any npm registry. Check your .npmrc file!
+
+
- S3
: S3 credential matcher
This matcher is a hostpath matcher.
diff --git a/docs/reference/ocm_logging.md b/docs/reference/ocm_logging.md
index 78950872ce..e1de158350 100644
--- a/docs/reference/ocm_logging.md
+++ b/docs/reference/ocm_logging.md
@@ -17,6 +17,7 @@ The following *tags* are used by the command line tool:
The following *realms* are used by the command line tool:
- ocm
: general realm used for the ocm go library.
+ - ocm/NPM
: NPM registry
- ocm/accessmethod/ociartifact
: access method ociArtifact
- ocm/compdesc
: component descriptor handling
- ocm/config
: configuration management
diff --git a/docs/reference/ocm_ocm-uploadhandlers.md b/docs/reference/ocm_ocm-uploadhandlers.md
index 161043fd17..767da82a6d 100644
--- a/docs/reference/ocm_ocm-uploadhandlers.md
+++ b/docs/reference/ocm_ocm-uploadhandlers.md
@@ -36,10 +36,6 @@ resource blob), it is possible to pass a target configuration controlling the
exact behaviour of the handler for selected artifacts.
The following handler names are possible:
- - plugin
: [downloaders provided by plugins]
-
- sub namespace of the form <plugin name>/<handler>
-
- ocm/ociArtifacts
: downloading OCI artifacts
The ociArtifacts
downloader is able to download OCI artifacts
@@ -64,6 +60,19 @@ The following handler names are possible:
Alternatively, a single string value can be given representing an OCI repository
reference.
+ - plugin
: [downloaders provided by plugins]
+
+ sub namespace of the form <plugin name>/<handler>
+
+ - ocm/npmPackage
: uploading npm artifacts
+
+ The ocm/npmPackage
uploader is able to upload npm artifacts
+ as artifact archive according to the npm package spec.
+ If registered the default mime type is: application/x-tgz
+
+ It accepts a plain string for the URL or a config with the following field:
+ 'url': the URL of the npm repository.
+
See [ocm ocm-uploadhandlers](ocm_ocm-uploadhandlers.md) for further details on using
diff --git a/docs/reference/ocm_transfer_commontransportarchive.md b/docs/reference/ocm_transfer_commontransportarchive.md
index 43a638641f..faa7bfe6a1 100644
--- a/docs/reference/ocm_transfer_commontransportarchive.md
+++ b/docs/reference/ocm_transfer_commontransportarchive.md
@@ -110,10 +110,6 @@ are configured for the operation. It has the following format
The uploader name may be a path expression with the following possibilities:
- - plugin
: [downloaders provided by plugins]
-
- sub namespace of the form <plugin name>/<handler>
-
- ocm/ociArtifacts
: downloading OCI artifacts
The ociArtifacts
downloader is able to download OCI artifacts
@@ -138,6 +134,19 @@ The uploader name may be a path expression with the following possibilities:
Alternatively, a single string value can be given representing an OCI repository
reference.
+ - plugin
: [downloaders provided by plugins]
+
+ sub namespace of the form <plugin name>/<handler>
+
+ - ocm/npmPackage
: uploading npm artifacts
+
+ The ocm/npmPackage
uploader is able to upload npm artifacts
+ as artifact archive according to the npm package spec.
+ If registered the default mime type is: application/x-tgz
+
+ It accepts a plain string for the URL or a config with the following field:
+ 'url': the URL of the npm repository.
+
See [ocm ocm-uploadhandlers](ocm_ocm-uploadhandlers.md) for further details on using
diff --git a/docs/reference/ocm_transfer_componentversions.md b/docs/reference/ocm_transfer_componentversions.md
index 31bea4db5d..cbbae63fe8 100644
--- a/docs/reference/ocm_transfer_componentversions.md
+++ b/docs/reference/ocm_transfer_componentversions.md
@@ -167,10 +167,6 @@ are configured for the operation. It has the following format
The uploader name may be a path expression with the following possibilities:
- - plugin
: [downloaders provided by plugins]
-
- sub namespace of the form <plugin name>/<handler>
-
- ocm/ociArtifacts
: downloading OCI artifacts
The ociArtifacts
downloader is able to download OCI artifacts
@@ -195,6 +191,19 @@ The uploader name may be a path expression with the following possibilities:
Alternatively, a single string value can be given representing an OCI repository
reference.
+ - plugin
: [downloaders provided by plugins]
+
+ sub namespace of the form <plugin name>/<handler>
+
+ - ocm/npmPackage
: uploading npm artifacts
+
+ The ocm/npmPackage
uploader is able to upload npm artifacts
+ as artifact archive according to the npm package spec.
+ If registered the default mime type is: application/x-tgz
+
+ It accepts a plain string for the URL or a config with the following field:
+ 'url': the URL of the npm repository.
+
See [ocm ocm-uploadhandlers](ocm_ocm-uploadhandlers.md) for further details on using
diff --git a/pkg/contexts/config/configutils/configure.go b/pkg/contexts/config/configutils/configure.go
index 9cb7068f06..b28c2a0920 100644
--- a/pkg/contexts/config/configutils/configure.go
+++ b/pkg/contexts/config/configutils/configure.go
@@ -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 configutils
import (
@@ -27,7 +23,7 @@ func Configure(path string) error {
func ConfigureContext(ctxp config.ContextProvider, path string) error {
ctx := config.FromProvider(ctxp)
- h := os.Getenv("HOME")
+ h, _ := os.UserHomeDir()
if path == "" {
if h != "" {
cfg := h + "/.ocmconfig"
diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/identity.go b/pkg/contexts/credentials/builtin/npm/identity/identity.go
similarity index 63%
rename from pkg/contexts/ocm/blobhandler/handlers/generic/npm/identity.go
rename to pkg/contexts/credentials/builtin/npm/identity/identity.go
index d6b5bb02e1..32f9a16e8c 100644
--- a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/identity.go
+++ b/pkg/contexts/credentials/builtin/npm/identity/identity.go
@@ -1,4 +1,4 @@
-package npm
+package identity
import (
"path"
@@ -9,13 +9,32 @@ import (
"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"
+ "github.com/open-component-model/ocm/pkg/logging"
)
+const (
+ // CONSUMER_TYPE is the npm repository type.
+ CONSUMER_TYPE = "Registry.npmjs.com"
+
+ // 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
+ // ATTR_TOKEN is the token attribute. May exist after login at any npm registry.
+ ATTR_TOKEN = cpi.ATTR_TOKEN
+)
+
+// Logging Realm.
+var REALM = logging.DefineSubRealm("NPM registry", "NPM")
+
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",
+ ATTR_TOKEN, "the token attribute. May exist after login at any npm registry. Check your .npmrc file!",
})
cpi.RegisterStandardIdentity(CONSUMER_TYPE, hostpath.IdentityMatcher(CONSUMER_TYPE), `NPM repository
diff --git a/pkg/contexts/credentials/repositories/dockerconfig/type.go b/pkg/contexts/credentials/repositories/dockerconfig/type.go
index bba00f371b..0d01494890 100644
--- a/pkg/contexts/credentials/repositories/dockerconfig/type.go
+++ b/pkg/contexts/credentials/repositories/dockerconfig/type.go
@@ -9,7 +9,9 @@ import (
"fmt"
"github.com/open-component-model/ocm/pkg/contexts/credentials/cpi"
+ "github.com/open-component-model/ocm/pkg/generics"
"github.com/open-component-model/ocm/pkg/runtime"
+ "github.com/open-component-model/ocm/pkg/utils"
)
const (
@@ -27,19 +29,19 @@ type RepositorySpec struct {
runtime.ObjectVersionedType `json:",inline"`
DockerConfigFile string `json:"dockerConfigFile,omitempty"`
DockerConfig json.RawMessage `json:"dockerConfig,omitempty"`
- PropgateConsumerIdentity bool `json:"propagateConsumerIdentity,omitempty"`
+ PropgateConsumerIdentity *bool `json:"propagateConsumerIdentity,omitempty"`
}
func (s RepositorySpec) WithConsumerPropagation(propagate bool) *RepositorySpec {
- s.PropgateConsumerIdentity = propagate
+ s.PropgateConsumerIdentity = &propagate
return &s
}
// NewRepositorySpec creates a new memory RepositorySpec.
func NewRepositorySpec(path string, prop ...bool) *RepositorySpec {
- p := false
- for _, e := range prop {
- p = p || e
+ var p *bool
+ if len(prop) > 0 {
+ p = generics.Pointer(utils.Optional(prop...))
}
if path == "" {
path = "~/.docker/config.json"
@@ -52,9 +54,9 @@ func NewRepositorySpec(path string, prop ...bool) *RepositorySpec {
}
func NewRepositorySpecForConfig(data []byte, prop ...bool) *RepositorySpec {
- p := false
- for _, e := range prop {
- p = p || e
+ var p *bool
+ if len(prop) > 0 {
+ p = generics.Pointer(utils.Optional(prop...))
}
return &RepositorySpec{
ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
@@ -73,5 +75,5 @@ func (a *RepositorySpec) Repository(ctx cpi.Context, creds cpi.Credentials) (cpi
if !ok {
return nil, fmt.Errorf("failed to assert type %T to Repositories", r)
}
- return repos.GetRepository(ctx, a.DockerConfigFile, a.DockerConfig, a.PropgateConsumerIdentity)
+ return repos.GetRepository(ctx, a.DockerConfigFile, a.DockerConfig, utils.AsBool(a.PropgateConsumerIdentity, true))
}
diff --git a/pkg/contexts/credentials/repositories/gardenerconfig/type.go b/pkg/contexts/credentials/repositories/gardenerconfig/type.go
index e57fd9f4d0..b7421da486 100644
--- a/pkg/contexts/credentials/repositories/gardenerconfig/type.go
+++ b/pkg/contexts/credentials/repositories/gardenerconfig/type.go
@@ -11,7 +11,9 @@ import (
"github.com/open-component-model/ocm/pkg/contexts/credentials/internal"
gardenercfgcpi "github.com/open-component-model/ocm/pkg/contexts/credentials/repositories/gardenerconfig/cpi"
"github.com/open-component-model/ocm/pkg/contexts/credentials/repositories/gardenerconfig/identity"
+ "github.com/open-component-model/ocm/pkg/generics"
"github.com/open-component-model/ocm/pkg/runtime"
+ "github.com/open-component-model/ocm/pkg/utils"
)
const (
@@ -30,19 +32,19 @@ type RepositorySpec struct {
URL string `json:"url"`
ConfigType gardenercfgcpi.ConfigType `json:"configType"`
Cipher Cipher `json:"cipher"`
- PropagateConsumerIdentity bool `json:"propagateConsumerIdentity"`
+ PropagateConsumerIdentity *bool `json:"propagateConsumerIdentity,omitempty"`
}
var _ cpi.ConsumerIdentityProvider = (*RepositorySpec)(nil)
// NewRepositorySpec creates a new memory RepositorySpec.
-func NewRepositorySpec(url string, configType gardenercfgcpi.ConfigType, cipher Cipher, propagateConsumerIdentity bool) *RepositorySpec {
+func NewRepositorySpec(url string, configType gardenercfgcpi.ConfigType, cipher Cipher, propagateConsumerIdentity ...bool) *RepositorySpec {
return &RepositorySpec{
ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
URL: url,
ConfigType: configType,
Cipher: cipher,
- PropagateConsumerIdentity: propagateConsumerIdentity,
+ PropagateConsumerIdentity: generics.Pointer(utils.OptionalDefaultedBool(true, propagateConsumerIdentity...)),
}
}
@@ -62,7 +64,7 @@ func (a *RepositorySpec) Repository(ctx cpi.Context, creds cpi.Credentials) (cpi
return nil, fmt.Errorf("unable to get key from context: %w", err)
}
- return repos.GetRepository(ctx, a.URL, a.ConfigType, a.Cipher, key, a.PropagateConsumerIdentity)
+ return repos.GetRepository(ctx, a.URL, a.ConfigType, a.Cipher, key, utils.AsBool(a.PropagateConsumerIdentity, true))
}
func (a *RepositorySpec) GetConsumerId(uctx ...internal.UsageContext) internal.ConsumerIdentity {
diff --git a/pkg/contexts/credentials/repositories/init.go b/pkg/contexts/credentials/repositories/init.go
index 84d8ad4bc8..fbdbb91bc1 100644
--- a/pkg/contexts/credentials/repositories/init.go
+++ b/pkg/contexts/credentials/repositories/init.go
@@ -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 repositories
import (
@@ -11,5 +7,6 @@ import (
_ "github.com/open-component-model/ocm/pkg/contexts/credentials/repositories/gardenerconfig"
_ "github.com/open-component-model/ocm/pkg/contexts/credentials/repositories/memory"
_ "github.com/open-component-model/ocm/pkg/contexts/credentials/repositories/memory/config"
+ _ "github.com/open-component-model/ocm/pkg/contexts/credentials/repositories/npm"
_ "github.com/open-component-model/ocm/pkg/contexts/credentials/repositories/vault"
)
diff --git a/pkg/contexts/credentials/repositories/npm/a_usage.go b/pkg/contexts/credentials/repositories/npm/a_usage.go
new file mode 100644
index 0000000000..a8b77fb9d7
--- /dev/null
+++ b/pkg/contexts/credentials/repositories/npm/a_usage.go
@@ -0,0 +1,18 @@
+package npm
+
+import (
+ "github.com/open-component-model/ocm/pkg/listformat"
+)
+
+var usage = `
+This repository type can be used to access credentials stored in a file
+following the NPM npmrc format (~/.npmrc). It take into account the
+credentials helper section, also. If enabled, the described
+credentials will be automatically assigned to appropriate consumer ids.
+`
+
+var format = `The repository specification supports the following fields:
+` + listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ "npmrcFile", "*string*: the file path to a NPM npmrc file",
+ "propagateConsumerIdentity", "*bool*(optional): enable consumer id propagation",
+})
diff --git a/pkg/contexts/credentials/repositories/npm/cache.go b/pkg/contexts/credentials/repositories/npm/cache.go
new file mode 100644
index 0000000000..15b2f0e74b
--- /dev/null
+++ b/pkg/contexts/credentials/repositories/npm/cache.go
@@ -0,0 +1,33 @@
+package npm
+
+import (
+ "github.com/open-component-model/ocm/pkg/contexts/credentials/cpi"
+ "github.com/open-component-model/ocm/pkg/contexts/datacontext"
+)
+
+type Cache struct {
+ repos map[string]*Repository
+}
+
+func createCache(_ datacontext.Context) interface{} {
+ return &Cache{
+ repos: map[string]*Repository{},
+ }
+}
+
+func (r *Cache) GetRepository(ctx cpi.Context, name string, prop bool) (*Repository, error) {
+ var (
+ err error = nil
+ repo *Repository
+ )
+ if name != "" {
+ repo = r.repos[name]
+ }
+ if repo == nil {
+ repo, err = NewRepository(ctx, name, prop)
+ if err == nil {
+ r.repos[name] = repo
+ }
+ }
+ return repo, err
+}
diff --git a/pkg/contexts/credentials/repositories/npm/config.go b/pkg/contexts/credentials/repositories/npm/config.go
new file mode 100644
index 0000000000..b6b750b9ea
--- /dev/null
+++ b/pkg/contexts/credentials/repositories/npm/config.go
@@ -0,0 +1,55 @@
+package npm
+
+import (
+ "bufio"
+ "os"
+ "strings"
+
+ "github.com/open-component-model/ocm/pkg/errors"
+ "github.com/open-component-model/ocm/pkg/utils"
+)
+
+type npmConfig map[string]string
+
+// readNpmConfigFile reads "~/.npmrc" file line by line, parse it and return the result as a npmConfig.
+func readNpmConfigFile(path string) (npmConfig, string, error) {
+ path, err := utils.ResolvePath(path)
+ if err != nil {
+ return nil, path, errors.Wrapf(err, "cannot resolve path %q", path)
+ }
+
+ // Open the file
+ file, err := os.Open(path)
+ if err != nil {
+ return nil, path, err
+ }
+ defer file.Close()
+
+ // Create a new scanner and read the file line by line
+ scanner := bufio.NewScanner(file)
+ cfg := make(map[string]string)
+ for scanner.Scan() {
+ line := scanner.Text()
+ line, authFound := strings.CutPrefix(line, "//")
+ if !authFound {
+ // e.g. 'global=false'
+ continue
+ }
+ // Split the line into key and value
+ parts := strings.SplitN(line, ":_authToken=", 2)
+ if len(parts) == 2 {
+ if strings.HasSuffix(parts[0], "/") {
+ cfg[parts[0][:len(parts[0])-1]] = parts[1]
+ } else {
+ cfg[parts[0]] = parts[1]
+ }
+ }
+ }
+
+ // Check for errors
+ if err = scanner.Err(); err != nil {
+ return nil, path, err
+ }
+
+ return cfg, path, nil
+}
diff --git a/pkg/contexts/credentials/repositories/npm/config_test.go b/pkg/contexts/credentials/repositories/npm/config_test.go
new file mode 100644
index 0000000000..e8cc8e28b3
--- /dev/null
+++ b/pkg/contexts/credentials/repositories/npm/config_test.go
@@ -0,0 +1,43 @@
+package npm_test
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "github.com/open-component-model/ocm/pkg/common"
+ "github.com/open-component-model/ocm/pkg/contexts/credentials"
+ "github.com/open-component-model/ocm/pkg/contexts/credentials/builtin/npm/identity"
+ "github.com/open-component-model/ocm/pkg/contexts/credentials/repositories/npm"
+ . "github.com/open-component-model/ocm/pkg/testutils"
+)
+
+var _ = Describe("Config deserialization Test Environment", func() {
+ It("read .npmrc", func() {
+ ctx := credentials.New()
+
+ repo := Must(npm.NewRepository(ctx, "testdata/.npmrc"))
+ Expect(Must(repo.LookupCredentials("registry.npmjs.org")).Properties()).To(Equal(common.Properties{identity.ATTR_TOKEN: "npm_TOKEN"}))
+ Expect(Must(repo.LookupCredentials("npm.registry.acme.com/api/npm")).Properties()).To(Equal(common.Properties{identity.ATTR_TOKEN: "bearer_TOKEN"}))
+ })
+
+ It("propagates credentials", func() {
+ ctx := credentials.New()
+
+ spec := npm.NewRepositorySpec("testdata/.npmrc")
+
+ _ = Must(ctx.RepositoryForSpec(spec))
+ id := identity.GetConsumerId("registry.npmjs.org", "pkg")
+
+ creds := Must(credentials.CredentialsForConsumer(ctx, id))
+ Expect(creds).NotTo(BeNil())
+ Expect(creds.GetProperty(identity.ATTR_TOKEN)).To(Equal("npm_TOKEN"))
+ })
+
+ It("has description", func() {
+ ctx := credentials.New()
+ t := ctx.RepositoryTypes().GetType(npm.TypeV1)
+ Expect(t).NotTo(BeNil())
+ Expect(t.Description()).NotTo(Equal(""))
+ })
+
+})
diff --git a/pkg/contexts/credentials/repositories/npm/default.go b/pkg/contexts/credentials/repositories/npm/default.go
new file mode 100644
index 0000000000..14c569f842
--- /dev/null
+++ b/pkg/contexts/credentials/repositories/npm/default.go
@@ -0,0 +1,53 @@
+package npm
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/mandelsoft/filepath/pkg/filepath"
+ "github.com/mandelsoft/vfs/pkg/osfs"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "github.com/open-component-model/ocm/pkg/contexts/config"
+ credcfg "github.com/open-component-model/ocm/pkg/contexts/credentials/config"
+ "github.com/open-component-model/ocm/pkg/contexts/ocm/utils/defaultconfigregistry"
+ "github.com/open-component-model/ocm/pkg/errors"
+)
+
+const (
+ ConfigFileName = ".npmrc"
+)
+
+func init() {
+ defaultconfigregistry.RegisterDefaultConfigHandler(DefaultConfigHandler, desc)
+}
+
+func DefaultConfig() (string, error) {
+ d, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(d, ConfigFileName), nil
+}
+
+func DefaultConfigHandler(cfg config.Context) error {
+ // use docker config as default config for ocm cli
+ d, err := DefaultConfig()
+ if err != nil {
+ return nil
+ }
+ if ok, err := vfs.FileExists(osfs.New(), d); ok && err == nil {
+ ccfg := credcfg.New()
+ ccfg.AddRepository(NewRepositorySpec(d, true))
+ err = cfg.ApplyConfig(ccfg, d)
+ if err != nil {
+ return errors.Wrapf(err, "cannot apply npm config %q", d)
+ }
+ }
+ return nil
+}
+
+var desc = fmt.Sprintf(`
+The npm configuration file at ~/%s
is
+read to feed in the configured credentials for NPM registries.
+`, ConfigFileName)
diff --git a/pkg/contexts/credentials/repositories/npm/provider.go b/pkg/contexts/credentials/repositories/npm/provider.go
new file mode 100644
index 0000000000..010dd12ac5
--- /dev/null
+++ b/pkg/contexts/credentials/repositories/npm/provider.go
@@ -0,0 +1,47 @@
+package npm
+
+import (
+ npm "github.com/open-component-model/ocm/pkg/contexts/credentials/builtin/npm/identity"
+ "github.com/open-component-model/ocm/pkg/contexts/credentials/cpi"
+ "github.com/open-component-model/ocm/pkg/logging"
+)
+
+type ConsumerProvider struct {
+ npmrcPath string
+}
+
+var _ cpi.ConsumerProvider = (*ConsumerProvider)(nil)
+
+func (p *ConsumerProvider) Unregister(_ cpi.ProviderIdentity) {
+}
+
+func (p *ConsumerProvider) Match(req cpi.ConsumerIdentity, cur cpi.ConsumerIdentity, m cpi.IdentityMatcher) (cpi.CredentialsSource, cpi.ConsumerIdentity) {
+ return p.get(req, cur, m)
+}
+
+func (p *ConsumerProvider) Get(req cpi.ConsumerIdentity) (cpi.CredentialsSource, bool) {
+ creds, _ := p.get(req, nil, cpi.CompleteMatch)
+ return creds, creds != nil
+}
+
+func (p *ConsumerProvider) get(requested cpi.ConsumerIdentity, currentFound cpi.ConsumerIdentity, m cpi.IdentityMatcher) (cpi.CredentialsSource, cpi.ConsumerIdentity) {
+ all, path, err := readNpmConfigFile(p.npmrcPath)
+ if err != nil {
+ log := logging.Context().Logger(npm.REALM)
+ log.LogError(err, "Failed to read npmrc file", "path", path)
+ return nil, nil
+ }
+
+ var creds cpi.CredentialsSource
+
+ for key, value := range all {
+ id := npm.GetConsumerId("https://"+key, "")
+
+ if m(requested, currentFound, id) {
+ creds = newCredentials(value)
+ currentFound = id
+ }
+ }
+
+ return creds, currentFound
+}
diff --git a/pkg/contexts/credentials/repositories/npm/repository.go b/pkg/contexts/credentials/repositories/npm/repository.go
new file mode 100644
index 0000000000..c02d7d5971
--- /dev/null
+++ b/pkg/contexts/credentials/repositories/npm/repository.go
@@ -0,0 +1,87 @@
+package npm
+
+import (
+ "fmt"
+
+ "github.com/open-component-model/ocm/pkg/common"
+ npmCredentials "github.com/open-component-model/ocm/pkg/contexts/credentials/builtin/npm/identity"
+ "github.com/open-component-model/ocm/pkg/contexts/credentials/cpi"
+ "github.com/open-component-model/ocm/pkg/errors"
+ "github.com/open-component-model/ocm/pkg/utils"
+)
+
+const PROVIDER = "ocm.software/credentialprovider/" + Type
+
+type Repository struct {
+ ctx cpi.Context
+ path string
+ propagate bool
+ npmrc npmConfig
+}
+
+func NewRepository(ctx cpi.Context, path string, prop ...bool) (*Repository, error) {
+ return newRepository(ctx, path, utils.OptionalDefaultedBool(true, prop...))
+}
+
+func newRepository(ctx cpi.Context, path string, prop bool) (*Repository, error) {
+ r := &Repository{
+ ctx: ctx,
+ path: path,
+ propagate: prop,
+ }
+ err := r.Read(true)
+ return r, err
+}
+
+var _ cpi.Repository = &Repository{}
+
+func (r *Repository) ExistsCredentials(name string) (bool, error) {
+ err := r.Read(false)
+ if err != nil {
+ return false, err
+ }
+ return r.npmrc[name] != "", nil
+}
+
+func (r *Repository) LookupCredentials(name string) (cpi.Credentials, error) {
+ exists, err := r.ExistsCredentials(name)
+ if err != nil {
+ return nil, err
+ }
+ if !exists {
+ return nil, errors.ErrNotFound("credentials", name, Type)
+ }
+ return newCredentials(r.npmrc[name]), nil
+}
+
+func (r *Repository) WriteCredentials(_ string, _ cpi.Credentials) (cpi.Credentials, error) {
+ return nil, errors.ErrNotSupported("write", "credentials", Type)
+}
+
+func (r *Repository) Read(force bool) error {
+ if !force && r.npmrc != nil {
+ return nil
+ }
+
+ if r.path == "" {
+ return fmt.Errorf("npmrc path not provided")
+ }
+ cfg, path, err := readNpmConfigFile(r.path)
+ if err != nil {
+ return fmt.Errorf("failed to load npmrc: %w", err)
+ }
+ id := cpi.ProviderIdentity(PROVIDER + "/" + path)
+
+ if r.propagate {
+ r.ctx.RegisterConsumerProvider(id, &ConsumerProvider{r.path})
+ }
+ r.npmrc = cfg
+ return nil
+}
+
+func newCredentials(token string) cpi.Credentials {
+ props := common.Properties{
+ npmCredentials.ATTR_TOKEN: token,
+ }
+ return cpi.NewCredentials(props)
+}
diff --git a/pkg/contexts/credentials/repositories/npm/repository_test.go b/pkg/contexts/credentials/repositories/npm/repository_test.go
new file mode 100644
index 0000000000..8de30de2b9
--- /dev/null
+++ b/pkg/contexts/credentials/repositories/npm/repository_test.go
@@ -0,0 +1,78 @@
+package npm_test
+
+import (
+ "encoding/json"
+ "reflect"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "github.com/open-component-model/ocm/pkg/common"
+ "github.com/open-component-model/ocm/pkg/contexts/credentials"
+ npmCredentials "github.com/open-component-model/ocm/pkg/contexts/credentials/builtin/npm/identity"
+ "github.com/open-component-model/ocm/pkg/contexts/credentials/cpi"
+ local "github.com/open-component-model/ocm/pkg/contexts/credentials/repositories/npm"
+ "github.com/open-component-model/ocm/pkg/finalizer"
+ . "github.com/open-component-model/ocm/pkg/testutils"
+)
+
+var _ = Describe("NPM config - .npmrc", func() {
+
+ props := common.Properties{
+ npmCredentials.ATTR_TOKEN: "npm_TOKEN",
+ }
+
+ props2 := common.Properties{
+ npmCredentials.ATTR_TOKEN: "bearer_TOKEN",
+ }
+
+ var DefaultContext credentials.Context
+
+ BeforeEach(func() {
+ DefaultContext = credentials.New()
+ })
+
+ specdata := "{\"type\":\"NPMConfig\",\"npmrcFile\":\"testdata/.npmrc\"}"
+
+ It("serializes repo spec", func() {
+ spec := local.NewRepositorySpec("testdata/.npmrc")
+ data := Must(json.Marshal(spec))
+ Expect(data).To(Equal([]byte(specdata)))
+ })
+
+ It("deserializes repo spec", func() {
+ spec := Must(DefaultContext.RepositorySpecForConfig([]byte(specdata), nil))
+ Expect(reflect.TypeOf(spec).String()).To(Equal("*npm.RepositorySpec"))
+ Expect(spec.(*local.RepositorySpec).NpmrcFile).To(Equal("testdata/.npmrc"))
+ })
+
+ It("resolves repository", func() {
+ repo := Must(DefaultContext.RepositoryForConfig([]byte(specdata), nil))
+ Expect(reflect.TypeOf(repo).String()).To(Equal("*npm.Repository"))
+ })
+
+ It("retrieves credentials", func() {
+ repo := Must(DefaultContext.RepositoryForConfig([]byte(specdata), nil))
+
+ creds := Must(repo.LookupCredentials("registry.npmjs.org"))
+ Expect(creds.Properties()).To(Equal(props))
+
+ creds = Must(repo.LookupCredentials("npm.registry.acme.com/api/npm"))
+ Expect(creds.Properties()).To(Equal(props2))
+ })
+
+ It("can access the default context", func() {
+ ctx := credentials.New()
+
+ r := finalizer.GetRuntimeFinalizationRecorder(ctx)
+ Expect(r).NotTo(BeNil())
+
+ Must(ctx.RepositoryForConfig([]byte(specdata), nil))
+
+ ci := cpi.NewConsumerIdentity(npmCredentials.CONSUMER_TYPE)
+ Expect(ci).NotTo(BeNil())
+ credentials := Must(cpi.CredentialsForConsumer(ctx.CredentialsContext(), ci))
+ Expect(credentials).NotTo(BeNil())
+ Expect(credentials.Properties()).To(Equal(props))
+ })
+})
diff --git a/pkg/contexts/credentials/repositories/npm/suite_test.go b/pkg/contexts/credentials/repositories/npm/suite_test.go
new file mode 100644
index 0000000000..e4947738d0
--- /dev/null
+++ b/pkg/contexts/credentials/repositories/npm/suite_test.go
@@ -0,0 +1,13 @@
+package npm_test
+
+import (
+ "testing"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+func TestConfig(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "NPM Repository tests")
+}
diff --git a/pkg/contexts/credentials/repositories/npm/testdata/.npmrc b/pkg/contexts/credentials/repositories/npm/testdata/.npmrc
new file mode 100644
index 0000000000..00ea1baa8f
--- /dev/null
+++ b/pkg/contexts/credentials/repositories/npm/testdata/.npmrc
@@ -0,0 +1,3 @@
+global=false
+//npm.registry.acme.com/api/npm/:_authToken=bearer_TOKEN
+//registry.npmjs.org/:_authToken=npm_TOKEN
diff --git a/pkg/contexts/credentials/repositories/npm/type.go b/pkg/contexts/credentials/repositories/npm/type.go
new file mode 100644
index 0000000000..32cf05b2f6
--- /dev/null
+++ b/pkg/contexts/credentials/repositories/npm/type.go
@@ -0,0 +1,61 @@
+package npm
+
+import (
+ "fmt"
+
+ "github.com/open-component-model/ocm/pkg/contexts/credentials/cpi"
+ "github.com/open-component-model/ocm/pkg/generics"
+ "github.com/open-component-model/ocm/pkg/runtime"
+ "github.com/open-component-model/ocm/pkg/utils"
+)
+
+const (
+ // Type is the type of the NPMConfig.
+ Type = "NPMConfig"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](Type))
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](TypeV1, cpi.WithDescription(usage), cpi.WithFormatSpec(format)))
+}
+
+// RepositorySpec describes a docker npmrc based credential repository interface.
+type RepositorySpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ NpmrcFile string `json:"npmrcFile,omitempty"`
+ PropgateConsumerIdentity *bool `json:"propagateConsumerIdentity,omitempty"`
+}
+
+// NewRepositorySpec creates a new memory RepositorySpec.
+func NewRepositorySpec(path string, propagate ...bool) *RepositorySpec {
+ var p *bool
+ if path == "" {
+ d, err := DefaultConfig()
+ if err == nil {
+ path = d
+ }
+ }
+ if len(propagate) > 0 {
+ p = generics.Pointer(utils.OptionalDefaultedBool(true, propagate...))
+ }
+
+ return &RepositorySpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ NpmrcFile: path,
+ PropgateConsumerIdentity: p,
+ }
+}
+
+func (rs *RepositorySpec) GetType() string {
+ return Type
+}
+
+func (rs *RepositorySpec) Repository(ctx cpi.Context, _ cpi.Credentials) (cpi.Repository, error) {
+ r := ctx.GetAttributes().GetOrCreateAttribute(".npmrc", createCache)
+ cache, ok := r.(*Cache)
+ if !ok {
+ return nil, fmt.Errorf("failed to assert type %T to Cache", r)
+ }
+ return cache.GetRepository(ctx, rs.NpmrcFile, utils.AsBool(rs.PropgateConsumerIdentity, true))
+}
diff --git a/pkg/contexts/ocm/attrs/plugindirattr/attr.go b/pkg/contexts/ocm/attrs/plugindirattr/attr.go
index faab56d98f..d33a758993 100644
--- a/pkg/contexts/ocm/attrs/plugindirattr/attr.go
+++ b/pkg/contexts/ocm/attrs/plugindirattr/attr.go
@@ -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 plugindirattr
import (
@@ -30,7 +26,7 @@ func init() {
}
func DefaultDir(fs vfs.FileSystem) string {
- home := os.Getenv("HOME")
+ home, _ := os.UserHomeDir()
if home != "" {
dir := filepath.Join(home, DEFAULT_PLUGIN_DIR)
if ok, err := vfs.DirExists(fs, dir); ok && err == nil {
diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/blobhandler.go b/pkg/contexts/ocm/blobhandler/handlers/generic/npm/blobhandler.go
index 2c244cdd1f..5e69bab218 100644
--- a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/blobhandler.go
+++ b/pkg/contexts/ocm/blobhandler/handlers/generic/npm/blobhandler.go
@@ -9,12 +9,15 @@ import (
"net/http"
"net/url"
+ npmCredentials "github.com/open-component-model/ocm/pkg/contexts/credentials/builtin/npm/identity"
"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"
)
+const BLOB_HANDLER_NAME = "ocm/npmPackage"
+
type artifactHandler struct {
spec *Config
}
@@ -49,7 +52,7 @@ func (b *artifactHandler) StoreBlob(blob cpi.BlobAccess, _ string, _ string, _ c
}
// read package.json from tarball to get name, version, etc.
- log := logging.Context().Logger(REALM)
+ log := logging.Context().Logger(npmCredentials.REALM)
log.Debug("reading package.json from tarball")
var pkg *Package
pkg, err = prepare(data)
@@ -61,19 +64,33 @@ func (b *artifactHandler) StoreBlob(blob cpi.BlobAccess, _ string, _ string, _ c
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
+ // get credentials and TODO cache it
+ cred := npmCredentials.GetCredentials(ctx.GetContext(), b.spec.Url, pkg.Name)
+ if cred == nil {
+ return nil, fmt.Errorf("No credentials found for %s. Couldn't upload '%s'.", b.spec.Url, pkg.Name)
+ }
+ log.Debug("found credentials")
+
+ // check if token exists, if not login and retrieve token
+ token := cred[npmCredentials.ATTR_TOKEN]
+ if token == "" {
+ // use user+pass+mail from credentials to login and retrieve bearer token
+ username := cred[npmCredentials.ATTR_USERNAME]
+ password := cred[npmCredentials.ATTR_PASSWORD]
+ email := cred[npmCredentials.ATTR_EMAIL]
+ if username == "" || password == "" || email == "" {
+ return nil, fmt.Errorf("No credentials for %s are invalid. Username, password or email missing! Couldn't upload '%s'.", b.spec.Url, pkg.Name)
+ }
+ log = log.WithValues("user", username, "repo", b.spec.Url)
+ log.Debug("login")
+
+ // TODO: check different kinds of .npmrc content
+ token, err = login(b.spec.Url, username, password, email)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ log.Debug("token found, skipping login")
}
// check if package exists
diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/const.go b/pkg/contexts/ocm/blobhandler/handlers/generic/npm/const.go
deleted file mode 100644
index e8f1271879..0000000000
--- a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/const.go
+++ /dev/null
@@ -1,22 +0,0 @@
-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")
diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/config_test.go b/pkg/contexts/ocm/blobhandler/handlers/generic/npm/registration_test.go
similarity index 85%
rename from pkg/contexts/ocm/blobhandler/handlers/generic/npm/config_test.go
rename to pkg/contexts/ocm/blobhandler/handlers/generic/npm/registration_test.go
index e6b891198a..d97f412c3a 100644
--- a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/config_test.go
+++ b/pkg/contexts/ocm/blobhandler/handlers/generic/npm/registration_test.go
@@ -13,12 +13,11 @@ 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"}))
+ Expect(cfg).To(Equal(&npm.Config{Url: "test"}))
})
It("deserializes struct", func() {
cfg := Must(registrations.DecodeConfig[npm.Config](`{"Url":"test"}`))
- Expect(cfg).To(Equal(&npm.Config{"test"}))
+ Expect(cfg).To(Equal(&npm.Config{Url: "test"}))
})
-
})
diff --git a/pkg/contexts/ocm/utils/configure.go b/pkg/contexts/ocm/utils/configure.go
index 467f9ff718..6a6cf297db 100644
--- a/pkg/contexts/ocm/utils/configure.go
+++ b/pkg/contexts/ocm/utils/configure.go
@@ -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 utils
import (
@@ -28,7 +24,7 @@ func Configure(ctx ocm.Context, path string, fss ...vfs.FileSystem) (ocm.Context
if ctx == nil {
ctx = ocm.DefaultContext()
}
- h := os.Getenv("HOME")
+ h, _ := os.UserHomeDir()
if path == "" {
if h != "" {
cfg := h + "/" + DEFAULT_OCM_CONFIG
diff --git a/pkg/contexts/ocm/utils/defaultconfigregistry/configure.go b/pkg/contexts/ocm/utils/defaultconfigregistry/configure.go
index 30fed05799..67f51b24c7 100644
--- a/pkg/contexts/ocm/utils/defaultconfigregistry/configure.go
+++ b/pkg/contexts/ocm/utils/defaultconfigregistry/configure.go
@@ -43,6 +43,20 @@ func (r *defaultConfigurationRegistry) Get() []DefaultConfigHandler {
return result
}
+func (r *defaultConfigurationRegistry) Description() string {
+ var result []string
+
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ for _, h := range defaultConfigRegistry.list {
+ if h.desc != "" {
+ result = append(result, strings.TrimSpace(h.desc))
+ }
+ }
+ return listformat.FormatDescriptionList("", result...)
+}
+
var defaultConfigRegistry = &defaultConfigurationRegistry{}
func RegisterDefaultConfigHandler(h DefaultConfigHandler, desc string) {
@@ -54,12 +68,5 @@ func Get() []DefaultConfigHandler {
}
func Description() string {
- var result []string
-
- for _, h := range defaultConfigRegistry.list {
- if h.desc != "" {
- result = append(result, strings.TrimSpace(h.desc))
- }
- }
- return listformat.FormatDescriptionList("", result...)
+ return defaultConfigRegistry.Description()
}
diff --git a/pkg/utils/path.go b/pkg/utils/path.go
index f56227a72a..1f1c14f724 100644
--- a/pkg/utils/path.go
+++ b/pkg/utils/path.go
@@ -1,7 +1,3 @@
-// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and Open Component Model contributors.
-//
-// SPDX-License-Identifier: Apache-2.0
-
package utils
import (
@@ -19,8 +15,8 @@ import (
// ResolvePath handles the ~ notation for the home directory.
func ResolvePath(path string) (string, error) {
if strings.HasPrefix(path, "~"+string(os.PathSeparator)) {
- home := os.Getenv("HOME")
- if home == "" {
+ home, err := os.UserHomeDir()
+ if home == "" || err != nil {
return path, fmt.Errorf("HOME not set")
}
path = home + path[1:]