-
Notifications
You must be signed in to change notification settings - Fork 416
feat: support fetching snapshot versions from a Maven registry #1160
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
Changes from all commits
0f66fcc
8568dad
55c66ac
7695961
c02a848
b0ff239
8e2379a
819e72d
498c859
a03125b
734b8a1
afa9407
83fcad5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,31 +24,97 @@ func NewMavenRegistryAPIClient(registry string) *MavenRegistryAPIClient { | |
|
||
var errAPIFailed = errors.New("API query failed") | ||
|
||
// GetProject fetches a pom.xml specified by groupID, artifactID and version and parses it to maven.Project. | ||
// For a snapshot version, version level metadata is used to find the extact version string. | ||
// More about Maven Repository Metadata Model: https://maven.apache.org/ref/3.9.9/maven-repository-metadata/ | ||
// More about Maven Metadata: https://maven.apache.org/repositories/metadata.html | ||
func (m *MavenRegistryAPIClient) GetProject(ctx context.Context, groupID, artifactID, version string) (maven.Project, error) { | ||
u, err := url.JoinPath(m.registry, strings.ReplaceAll(groupID, ".", "/"), artifactID, version, fmt.Sprintf("%s-%s.pom", artifactID, version)) | ||
if !strings.HasSuffix(version, "-SNAPSHOT") { | ||
return m.getProject(ctx, groupID, artifactID, version, "") | ||
} | ||
|
||
// Fetch version metadata for snapshot versions. | ||
metadata, err := m.getVersionMetadata(ctx, groupID, artifactID, version) | ||
if err != nil { | ||
return maven.Project{}, err | ||
} | ||
|
||
snapshot := "" | ||
for _, sv := range metadata.Versioning.SnapshotVersions { | ||
if sv.Extension == "pom" { | ||
// We only look for pom.xml for project metadata. | ||
Comment on lines
+44
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you know what happens if the e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know exact answer to this question. A snapshot version is the version in development before release and the actual version string is the timestamp + build number - I would expect them to be the same for the same snapshot version. |
||
snapshot = string(sv.Value) | ||
break | ||
} | ||
} | ||
|
||
return m.getProject(ctx, groupID, artifactID, version, snapshot) | ||
} | ||
|
||
// getProject fetches a pom.xml specified by groupID, artifactID and version and parses it to maven.Project. | ||
// For snapshot versions, the exact version value is specified by snapshot. | ||
func (m *MavenRegistryAPIClient) getProject(ctx context.Context, groupID, artifactID, version, snapshot string) (maven.Project, error) { | ||
if snapshot == "" { | ||
snapshot = version | ||
} | ||
u, err := url.JoinPath(m.registry, strings.ReplaceAll(groupID, ".", "/"), artifactID, version, fmt.Sprintf("%s-%s.pom", artifactID, snapshot)) | ||
if err != nil { | ||
return maven.Project{}, fmt.Errorf("failed to join path: %w", err) | ||
} | ||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) | ||
var proj maven.Project | ||
if err := get(ctx, u, &proj); err != nil { | ||
return maven.Project{}, err | ||
} | ||
|
||
return proj, nil | ||
} | ||
|
||
// getVersionMetadata fetches a version level maven-metadata.xml and parses it to maven.Metadata. | ||
func (m *MavenRegistryAPIClient) getVersionMetadata(ctx context.Context, groupID, artifactID, version string) (maven.Metadata, error) { | ||
u, err := url.JoinPath(m.registry, strings.ReplaceAll(groupID, ".", "/"), artifactID, version, "maven-metadata.xml") | ||
if err != nil { | ||
return maven.Metadata{}, fmt.Errorf("failed to join path: %w", err) | ||
} | ||
|
||
var metadata maven.Metadata | ||
if err := get(ctx, u, &metadata); err != nil { | ||
return maven.Metadata{}, err | ||
} | ||
|
||
return metadata, nil | ||
} | ||
|
||
// GetArtifactMetadata fetches an artifact level maven-metadata.xml and parses it to maven.Metadata. | ||
func (m *MavenRegistryAPIClient) GetArtifactMetadata(ctx context.Context, groupID, artifactID string) (maven.Metadata, error) { | ||
u, err := url.JoinPath(m.registry, strings.ReplaceAll(groupID, ".", "/"), artifactID, "maven-metadata.xml") | ||
if err != nil { | ||
return maven.Project{}, fmt.Errorf("failed to make new request: %w", err) | ||
return maven.Metadata{}, fmt.Errorf("failed to join path: %w", err) | ||
} | ||
|
||
var metadata maven.Metadata | ||
if err := get(ctx, u, &metadata); err != nil { | ||
return maven.Metadata{}, err | ||
} | ||
|
||
return metadata, nil | ||
} | ||
|
||
func get(ctx context.Context, url string, dst interface{}) error { | ||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) | ||
if err != nil { | ||
return fmt.Errorf("failed to make new request: %w", err) | ||
} | ||
|
||
resp, err := http.DefaultClient.Do(req) | ||
if err != nil { | ||
return maven.Project{}, fmt.Errorf("%w: Maven registry query failed: %w", errAPIFailed, err) | ||
return fmt.Errorf("%w: Maven registry query failed: %w", errAPIFailed, err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return maven.Project{}, fmt.Errorf("%w: Maven registry query status: %s", errAPIFailed, resp.Status) | ||
return fmt.Errorf("%w: Maven registry query status: %s", errAPIFailed, resp.Status) | ||
} | ||
|
||
var proj maven.Project | ||
if err := xml.NewDecoder(resp.Body).Decode(&proj); err != nil { | ||
return maven.Project{}, fmt.Errorf("failed to decode Maven project: %w", err) | ||
} | ||
|
||
return proj, nil | ||
return xml.NewDecoder(resp.Body).Decode(dst) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
package datasource | ||
|
||
import ( | ||
"context" | ||
"reflect" | ||
"testing" | ||
|
||
"deps.dev/util/maven" | ||
"github.com/google/osv-scanner/internal/testutility" | ||
) | ||
|
||
func TestGetProject(t *testing.T) { | ||
t.Parallel() | ||
|
||
srv := testutility.NewMockHTTPServer(t) | ||
client := &MavenRegistryAPIClient{ | ||
registry: srv.URL, | ||
} | ||
srv.SetResponse(t, "org/example/x.y.z/1.0.0/x.y.z-1.0.0.pom", []byte(` | ||
<project> | ||
<groupId>org.example</groupId> | ||
<artifactId>x.y.z</artifactId> | ||
<version>1.0.0</version> | ||
</project> | ||
`)) | ||
|
||
got, err := client.GetProject(context.Background(), "org.example", "x.y.z", "1.0.0") | ||
if err != nil { | ||
t.Fatalf("failed to get Maven project %s:%s verion %s: %v", "org.example", "x.y.z", "1.0.0", err) | ||
} | ||
want := maven.Project{ | ||
ProjectKey: maven.ProjectKey{ | ||
GroupID: "org.example", | ||
ArtifactID: "x.y.z", | ||
Version: "1.0.0", | ||
}, | ||
} | ||
if !reflect.DeepEqual(got, want) { | ||
t.Errorf("GetProject(%s, %s, %s):\ngot %v\nwant %v\n", "org.example", "x.y.z", "1.0.0", got, want) | ||
} | ||
} | ||
|
||
func TestGetProjectSnapshot(t *testing.T) { | ||
t.Parallel() | ||
|
||
srv := testutility.NewMockHTTPServer(t) | ||
client := &MavenRegistryAPIClient{ | ||
registry: srv.URL, | ||
} | ||
srv.SetResponse(t, "org/example/x.y.z/3.3.1-SNAPSHOT/maven-metadata.xml", []byte(` | ||
<metadata> | ||
<groupId>org.example</groupId> | ||
<artifactId>x.y.z</artifactId> | ||
<versioning> | ||
<snapshot> | ||
<timestamp>20230302.052731</timestamp> | ||
<buildNumber>9</buildNumber> | ||
</snapshot> | ||
<lastUpdated>20230302052731</lastUpdated> | ||
<snapshotVersions> | ||
<snapshotVersion> | ||
<extension>jar</extension> | ||
<value>3.3.1-20230302.052731-9</value> | ||
<updated>20230302052731</updated> | ||
</snapshotVersion> | ||
<snapshotVersion> | ||
<extension>pom</extension> | ||
<value>3.3.1-20230302.052731-9</value> | ||
<updated>20230302052731</updated> | ||
</snapshotVersion> | ||
</snapshotVersions> | ||
</versioning> | ||
</metadata> | ||
`)) | ||
srv.SetResponse(t, "org/example/x.y.z/3.3.1-SNAPSHOT/x.y.z-3.3.1-20230302.052731-9.pom", []byte(` | ||
<project> | ||
<groupId>org.example</groupId> | ||
<artifactId>x.y.z</artifactId> | ||
<version>3.3.1-SNAPSHOT</version> | ||
</project> | ||
`)) | ||
|
||
got, err := client.GetProject(context.Background(), "org.example", "x.y.z", "3.3.1-SNAPSHOT") | ||
if err != nil { | ||
t.Fatalf("failed to get Maven project %s:%s verion %s: %v", "org.example", "x.y.z", "3.3.1-SNAPSHOT", err) | ||
} | ||
want := maven.Project{ | ||
ProjectKey: maven.ProjectKey{ | ||
GroupID: "org.example", | ||
ArtifactID: "x.y.z", | ||
Version: "3.3.1-SNAPSHOT", | ||
}, | ||
} | ||
if !reflect.DeepEqual(got, want) { | ||
t.Errorf("GetProject(%s, %s, %s):\ngot %v\nwant %v\n", "org.example", "x.y.z", "3.3.1-SNAPSHOT", got, want) | ||
} | ||
} | ||
|
||
func TestGetArtifactMetadata(t *testing.T) { | ||
t.Parallel() | ||
|
||
srv := testutility.NewMockHTTPServer(t) | ||
client := &MavenRegistryAPIClient{ | ||
registry: srv.URL, | ||
} | ||
srv.SetResponse(t, "org/example/x.y.z/maven-metadata.xml", []byte(` | ||
<metadata> | ||
<groupId>org.example</groupId> | ||
<artifactId>x.y.z</artifactId> | ||
<versioning> | ||
<latest>3.0</latest> | ||
<release>3.0</release> | ||
<versions> | ||
<version>1.0</version> | ||
<version>2.0</version> | ||
<version>3.0</version> | ||
</versions> | ||
</versioning> | ||
</metadata> | ||
`)) | ||
|
||
got, err := client.GetArtifactMetadata(context.Background(), "org.example", "x.y.z") | ||
if err != nil { | ||
t.Fatalf("failed to get artifact metadata for %s:%s: %v", "org.example", "x.y.z", err) | ||
} | ||
want := maven.Metadata{ | ||
GroupID: "org.example", | ||
ArtifactID: "x.y.z", | ||
Versioning: maven.Versioning{ | ||
Latest: "3.0", | ||
Release: "3.0", | ||
Versions: []maven.String{ | ||
"1.0", | ||
"2.0", | ||
"3.0", | ||
}, | ||
}, | ||
} | ||
if !reflect.DeepEqual(got, want) { | ||
t.Errorf("GetArtifactMetadata(%s, %s):\ngot %v\nwant %v\n", "org.example", "x.y.z", got, want) | ||
} | ||
} | ||
|
||
func TestGetVersionMetadata(t *testing.T) { | ||
t.Parallel() | ||
|
||
srv := testutility.NewMockHTTPServer(t) | ||
client := &MavenRegistryAPIClient{ | ||
registry: srv.URL, | ||
} | ||
srv.SetResponse(t, "org/example/x.y.z/3.3.1-SNAPSHOT/maven-metadata.xml", []byte(` | ||
<metadata> | ||
<groupId>org.example</groupId> | ||
<artifactId>x.y.z</artifactId> | ||
<versioning> | ||
<snapshot> | ||
<timestamp>20230302.052731</timestamp> | ||
<buildNumber>9</buildNumber> | ||
</snapshot> | ||
<lastUpdated>20230302052731</lastUpdated> | ||
<snapshotVersions> | ||
<snapshotVersion> | ||
<extension>jar</extension> | ||
<value>3.3.1-20230302.052731-9</value> | ||
<updated>20230302052731</updated> | ||
</snapshotVersion> | ||
<snapshotVersion> | ||
<extension>pom</extension> | ||
<value>3.3.1-20230302.052731-9</value> | ||
<updated>20230302052731</updated> | ||
</snapshotVersion> | ||
</snapshotVersions> | ||
</versioning> | ||
</metadata> | ||
`)) | ||
|
||
got, err := client.getVersionMetadata(context.Background(), "org.example", "x.y.z", "3.3.1-SNAPSHOT") | ||
if err != nil { | ||
t.Fatalf("failed to get metadata for %s:%s verion %s: %v", "org.example", "x.y.z", "3.3.1-SNAPSHOT", err) | ||
} | ||
want := maven.Metadata{ | ||
GroupID: "org.example", | ||
ArtifactID: "x.y.z", | ||
Versioning: maven.Versioning{ | ||
Snapshot: maven.Snapshot{ | ||
Timestamp: "20230302.052731", | ||
BuildNumber: 9, | ||
}, | ||
LastUpdated: "20230302052731", | ||
SnapshotVersions: []maven.SnapshotVersion{ | ||
{ | ||
Extension: "jar", | ||
Value: "3.3.1-20230302.052731-9", | ||
Updated: "20230302052731", | ||
}, | ||
{ | ||
Extension: "pom", | ||
Value: "3.3.1-20230302.052731-9", | ||
Updated: "20230302052731", | ||
}, | ||
}, | ||
}, | ||
} | ||
if !reflect.DeepEqual(got, want) { | ||
t.Errorf("getVersionMetadata(%s, %s):\ngot %v\nwant %v\n", "org.example", "x.y.z", got, want) | ||
} | ||
} |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it valid for a snapshot version directory to not have a
maven-metadata.xml
file? If it is, would maven just look forpkg-1.2.3-SNAPSHOT.pom
?Conversely, is it possible for a non-snapshot version to have a metadata file with
snapshotVersions
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure if it is valid for a snapshot version to not have a metadata file.
Based on the link in the comment above, a non-snapshot version should not have a metadata file with
snapshotVersion
.