Skip to content

Commit e92c377

Browse files
authored
Merge branch 'main' into RTECO-1648-apm-cli-integration-tests
2 parents 2619b2c + 4c19791 commit e92c377

21 files changed

Lines changed: 2426 additions & 20 deletions

artifactory/commands/npm/artifactoryinstall.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ func (nri *npmRtInstall) Run() (err error) {
2323
if err = nri.CreateTempNpmrc(); err != nil {
2424
return
2525
}
26+
if err = nri.applyZeroTouchRemediation(); err != nil {
27+
return
28+
}
2629
if err = nri.prepareBuildInfoModule(); err != nil {
2730
return
2831
}

artifactory/commands/npm/npmcommand.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ type NpmCommand struct {
7373
collectBuildInfo bool
7474
buildInfoModule *build.NpmModule
7575
installHandler *NpmInstallStrategy
76+
// When true, the subsequent install uses npm ci to honor remediated lockfile integrity.
77+
remediatedLockfile bool
78+
// Restores lockfiles written by Zero Touch Remediation if the install command fails.
79+
restoreResolution func() error
7680
// When true, skips the 404 error handling that checks if packages are blocked by curation
7781
disableCVSCheck bool
7882
}
@@ -351,12 +355,20 @@ func (nc *NpmCommand) Run() (err error) {
351355
defer func() {
352356
err = errors.Join(err, nc.installHandler.RestoreNpmrc())
353357
}()
358+
err = nc.installWithLockfileRestore()
359+
return
360+
}
361+
362+
func (nc *NpmCommand) installWithLockfileRestore() (err error) {
363+
defer func() {
364+
if err != nil && nc.restoreResolution != nil {
365+
err = errors.Join(err, nc.restoreResolution())
366+
}
367+
}()
354368
err = nc.installHandler.Install()
355-
if err != nil {
356-
if !nc.disableCVSCheck && (nc.cmdName == "install" || nc.cmdName == "ci") {
357-
if blockedErr := nc.handle404Errors(err); blockedErr != nil {
358-
err = blockedErr
359-
}
369+
if err != nil && !nc.disableCVSCheck && (nc.cmdName == "install" || nc.cmdName == "ci") {
370+
if blockedErr := nc.handle404Errors(err); blockedErr != nil {
371+
err = blockedErr
360372
}
361373
}
362374
return
@@ -508,8 +520,21 @@ func (nc *NpmCommand) prepareBuildInfoModule() error {
508520
return nil
509521
}
510522

523+
func (nc *NpmCommand) dependencyCollectionArgs() []string {
524+
npmArgs := nc.npmArgs
525+
npmCommand := nc.cmdName
526+
if nc.remediatedLockfile && nc.cmdName == "install" {
527+
npmCommand = "ci"
528+
npmArgs = stripNpmInstallOnlyArgs(npmArgs)
529+
}
530+
return append([]string{npmCommand}, npmArgs...)
531+
}
532+
511533
func (nc *NpmCommand) collectDependencies() error {
512-
nc.buildInfoModule.SetNpmArgs(append([]string{nc.cmdName}, nc.npmArgs...))
534+
if nc.remediatedLockfile && nc.cmdName == "install" {
535+
log.Info("Using npm ci after Zero Touch Remediation to install from the remediated lockfile")
536+
}
537+
nc.buildInfoModule.SetNpmArgs(nc.dependencyCollectionArgs())
513538
return errorutils.CheckError(nc.buildInfoModule.Build())
514539
}
515540

artifactory/commands/npm/npminstall.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ func (ni *npmInstall) PrepareInstallPrerequisites(repo string) error {
1212
}
1313

1414
func (ni *npmInstall) Run() (err error) {
15+
if err = ni.applyZeroTouchRemediation(); err != nil {
16+
return
17+
}
1518
if err = ni.prepareBuildInfoModule(); err != nil {
1619
return
1720
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package npm
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sort"
7+
"strings"
8+
9+
biUtils "github.com/jfrog/build-info-go/build/utils"
10+
npmUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/npm"
11+
"github.com/jfrog/jfrog-cli-core/v2/utils/xray"
12+
"github.com/jfrog/jfrog-client-go/utils/log"
13+
14+
"github.com/jfrog/jfrog-cli-artifactory/artifactory/zerotouchremediation"
15+
)
16+
17+
func (nc *NpmCommand) applyZeroTouchRemediation() error {
18+
if !zerotouchremediation.IsComponentResolutionEnabled() {
19+
return nil
20+
}
21+
restore, remediated, err := nc.runZeroTouchRemediation(context.Background(), nc.cmdName, nc.workingDirectory, nc.npmArgs)
22+
if err != nil {
23+
return err
24+
}
25+
nc.restoreResolution = restore
26+
nc.remediatedLockfile = remediated
27+
return nil
28+
}
29+
30+
func (nc *NpmCommand) runZeroTouchRemediation(ctx context.Context, command, workingDir string, npmArgs []string) (restore func() error, remediated bool, err error) {
31+
parsedArgs := parseNpmCLIArgs(npmArgs)
32+
if command == "install" && len(parsedArgs.packageOperands) > 0 {
33+
return func() error { return nil }, false, nil
34+
}
35+
resolverRepo, resolverErr := nc.resolverRepoForResolution(parsedArgs.registryURL)
36+
if resolverErr != nil {
37+
return zerotouchremediation.SkipRemediation("Zero Touch Remediation skipped: could not determine resolver repo: ", resolverErr)
38+
}
39+
if resolverRepo == "" {
40+
return zerotouchremediation.SkipRemediation("Zero Touch Remediation skipped: resolver repo is empty", nil)
41+
}
42+
var projectKey string
43+
if nc.buildConfiguration != nil {
44+
projectKey = nc.buildConfiguration.GetProject()
45+
}
46+
xrayManager, xrayErr := xray.CreateXrayServiceManager(nc.serverDetails, xray.WithScopedProjectKey(projectKey))
47+
if xrayErr != nil {
48+
return zerotouchremediation.SkipRemediation("Zero Touch Remediation skipped: could not create Xray service manager: ", xrayErr)
49+
}
50+
tool := BuildTool{opts: discoveryOptions{prefixDir: parsedArgs.prefixDir}}
51+
runner := func(_ context.Context, projectRoot string, args ...string) error {
52+
_, _, err := biUtils.RunNpmCmd(nc.executablePath, projectRoot, args, log.Logger)
53+
return err
54+
}
55+
return zerotouchremediation.RunIfEnabled(ctx, xrayManager, resolverRepo, tool, command, workingDir, runner, parsedArgs.bootstrapArgs...)
56+
}
57+
58+
// resolverRepoForResolution returns the Artifactory virtual repo for dependency policy scope.
59+
// Native npmrc can set both registry and @scope:registry; Xray accepts one repo, so this
60+
// uses the unique Artifactory npm repo across those URLs and errors if more than one exists.
61+
func (nc *NpmCommand) resolverRepoForResolution(registryURL string) (string, error) {
62+
if registryURL == "" && nc.repo != "" {
63+
return nc.repo, nil
64+
}
65+
listedConfig := false
66+
if nc.executablePath != "" {
67+
if data, err := npmUtils.GetConfigList(nc.npmArgs, nc.executablePath); err == nil {
68+
listedConfig = true
69+
repo, repoErr := resolverRepoFromNpmConfig(registryURL, data)
70+
if repoErr != nil {
71+
return "", repoErr
72+
}
73+
if repo != "" {
74+
return repo, nil
75+
}
76+
}
77+
}
78+
if registryURL != "" {
79+
return extractRepoName(registryURL)
80+
}
81+
if nc.repo != "" {
82+
return nc.repo, nil
83+
}
84+
if listedConfig || nc.executablePath == "" {
85+
return "", nil
86+
}
87+
registryURL, err := npmUtils.ConfigGet(nc.npmArgs, "registry", nc.executablePath)
88+
if err != nil {
89+
return "", fmt.Errorf("failed to get registry URL: %w", err)
90+
}
91+
return extractRepoName(registryURL)
92+
}
93+
94+
func resolverRepoFromNpmConfig(cliRegistryURL string, configList []byte) (string, error) {
95+
repos := uniqueArtifactoryNpmRepos(npmConfigRegistryURLs(cliRegistryURL, configList))
96+
switch len(repos) {
97+
case 0:
98+
return "", nil
99+
case 1:
100+
return repos[0], nil
101+
default:
102+
return "", fmt.Errorf("multiple Artifactory npm registries in npm config: %s", strings.Join(repos, ", "))
103+
}
104+
}
105+
106+
func npmConfigRegistryURLs(cliRegistryURL string, configList []byte) []string {
107+
cliOverridesDefault := cliRegistryURL != ""
108+
var urls []string
109+
if cliOverridesDefault {
110+
urls = append(urls, cliRegistryURL)
111+
}
112+
for _, rawLine := range strings.Split(string(configList), "\n") {
113+
line := strings.TrimSpace(rawLine)
114+
if line == "" || strings.HasPrefix(line, ";") {
115+
continue
116+
}
117+
key, value, ok := strings.Cut(line, "=")
118+
if !ok {
119+
continue
120+
}
121+
key = strings.TrimSpace(key)
122+
value = strings.Trim(strings.TrimSpace(value), `"`)
123+
if value == "" || value == "undefined" {
124+
continue
125+
}
126+
switch {
127+
case key == "registry" && !cliOverridesDefault:
128+
urls = append(urls, value)
129+
case strings.HasPrefix(key, "@") && strings.HasSuffix(key, ":registry"):
130+
urls = append(urls, value)
131+
}
132+
}
133+
return urls
134+
}
135+
136+
func uniqueArtifactoryNpmRepos(urls []string) []string {
137+
seen := make(map[string]struct{})
138+
var repos []string
139+
for _, raw := range urls {
140+
if !isArtifactoryNpmRegistryURL(raw) {
141+
continue
142+
}
143+
repo, err := extractRepoName(raw)
144+
if err != nil || repo == "" {
145+
continue
146+
}
147+
if _, ok := seen[repo]; ok {
148+
continue
149+
}
150+
seen[repo] = struct{}{}
151+
repos = append(repos, repo)
152+
}
153+
sort.Strings(repos)
154+
return repos
155+
}
156+
157+
func isArtifactoryNpmRegistryURL(registryURL string) bool {
158+
return strings.Contains(registryURL, "/api/npm/") || strings.Contains(registryURL, "/artifactory/")
159+
}

0 commit comments

Comments
 (0)