generate json schema from struct definitions

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2020-11-20 07:14:36 -05:00
parent 0ed30138c4
commit 8a17bfb69f
No known key found for this signature in database
GPG key ID: 5CB45AE22BAB7EA7
15 changed files with 228 additions and 534 deletions

View file

@ -153,17 +153,9 @@ fixtures:
cd syft/cataloger/java/test-fixtures/java-builds && make
.PHONY: generate-json-schema
generate-json-schema: clean-json-schema-examples integration ## Generate a new json schema for the json presenter, derived from integration test cases
docker run \
-i \
--rm \
-v $(shell pwd)/schema/json:/work \
-w /work \
python:3.8 \
bash -x -c "\
pip install -r requirements.txt && \
python generate.py \
"
generate-json-schema: ## Generate a new json schema
cd schema/json
go run generate.go
.PHONY: clear-test-cache
clear-test-cache: ## Delete all test cache (built docker image tars)
@ -288,7 +280,7 @@ release: clean-dist ci-bootstrap-mac changelog-release ## Build and publish fina
.github/scripts/update-version-file.sh "$(DISTDIR)" "$(VERSION)"
.PHONY: clean
clean: clean-dist clean-snapshot clean-json-schema-examples ## Remove previous builds and result reports
clean: clean-dist clean-snapshot ## Remove previous builds and result reports
rm -rf $(RESULTSDIR)/*
.PHONY: clean-snapshot
@ -298,7 +290,3 @@ clean-snapshot:
.PHONY: clean-dist
clean-dist:
rm -rf $(DISTDIR) $(TEMPDIR)/goreleaser.yaml
.PHONY: clean-json-schema-examples
clean-json-schema-examples:
rm -f schema/json/examples/*

1
go.mod
View file

@ -4,6 +4,7 @@ go 1.14
require (
github.com/adrg/xdg v0.2.1
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921
github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b

5
go.sum
View file

@ -119,6 +119,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/adrg/xdg v0.2.1 h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=
github.com/adrg/xdg v0.2.1/go.mod h1:ZuOshBmzV4Ta+s23hdfFZnBsdzmoR3US0d7ErpqSbTQ=
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921 h1:T3+cD5fYvuH36h7EZq+TDpm+d8a6FSD4pQsbmuGGQ8o=
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60=
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@ -501,6 +503,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk=
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
@ -787,6 +791,7 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=

View file

@ -1,9 +0,0 @@
## Updating the JSON schema
Today the JSON schema is generated from integration test data. Specifically, when integration tests are run, the `/schema/json/examples` directory is populated with syft JSON output data. This examples directory is used to drive automatically generating the JSON schema.
The caveats with this approach is:
1) the JSON schema is only as good as the examples provided
2) there is an integration test that ensures that the JSON schema is valid relative to what the code currently generates.
This means to update the JSON schema you need to
1) Open up `test/integration/json_schema_test.go` and comment out invocations of the `validateAgainstV1Schema` function.
2) From the root of the repo run `generate-json-schema`. Now there should be a new schema generated at `/schema/json/schema.json`
3) Uncomment the `validateAgainstV1Schema` function.

View file

@ -1 +0,0 @@
examples/

35
schema/json/generate.go Normal file
View file

@ -0,0 +1,35 @@
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/alecthomas/jsonschema"
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
)
/*
This method of creating the JSON schema only captures strongly typed fields for the purpose of integrations between syft
JSON output and integrations. The downside to this approach is that any values and types used on weakly typed fields
are not captured (empty interfaces). This means that pkg.Package.Metadata is not validated at this time. This approach
can be extended to include specific package metadata struct shapes in the future.
*/
func main() {
j := jsonschema.Reflect(&jsonPresenter.Document{})
filename := "schema.json"
fh, err := os.OpenFile("schema.json", os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
panic(err)
}
enc := json.NewEncoder(fh)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
err = enc.Encode(&j)
if err != nil {
panic(err)
}
fmt.Printf("wrote new schema to %q\n", filename)
}

View file

@ -1,30 +0,0 @@
#!/usr/env/bin python3
import os
import glob
import json
from genson import SchemaBuilder
EXAMPLES_DIR = "examples/"
OUTPUT = "schema.json"
def main():
builder = SchemaBuilder()
print("Generating new Syft json schema...")
for filepath in glob.glob(os.path.join(EXAMPLES_DIR, '*.json')):
with open(filepath, 'r') as f:
print(f" adding {filepath}")
builder.add_object(json.loads(f.read()))
print("Building schema...")
new_schema = builder.to_schema()
with open(OUTPUT, 'w') as f:
f.write(json.dumps(new_schema, sort_keys=True, indent=4))
print(f"New schema written to '{OUTPUT}'")
if __name__ == "__main__":
main()

View file

@ -1 +0,0 @@
genson

View file

@ -1,455 +1,165 @@
{
"$schema": "http://json-schema.org/schema#",
"properties": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Document",
"definitions": {
"Descriptor": {
"required": [
"name",
"version"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"Distribution": {
"required": [
"name",
"version",
"idLike"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"idLike": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"Document": {
"required": [
"artifacts",
"source",
"distro",
"descriptor"
],
"properties": {
"artifacts": {
"items": {
"properties": {
"foundBy": {
"type": "string"
},
"language": {
"type": "string"
},
"licenses": {
"anyOf": [
{
"type": "null"
},
{
"items": {
"type": "string"
},
"type": "array"
}
]
},
"locations": {
"items": {
"properties": {
"layerID": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
},
"type": "array"
},
"metadata": {
"properties": {
"architecture": {
"type": "string"
},
"author": {
"type": "string"
},
"authorEmail": {
"type": "string"
},
"description": {
"type": "string"
},
"epoch": {
"type": "integer"
},
"files": {
"anyOf": [
{
"type": "null"
},
{
"items": {
"anyOf": [
{
"type": "string"
},
{
"properties": {
"checksum": {
"type": "string"
},
"digest": {
"properties": {
"algorithm": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"algorithm",
"value"
],
"type": "object"
},
"ownerGid": {
"type": "string"
},
"ownerUid": {
"type": "string"
},
"path": {
"type": "string"
},
"permissions": {
"type": "string"
},
"size": {
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
]
},
"type": "array"
}
]
},
"gitCommitOfApkPort": {
"type": "string"
},
"homepage": {
"type": "string"
},
"installedSize": {
"type": "integer"
},
"license": {
"type": "string"
},
"licenses": {
"items": {
"type": "string"
},
"type": "array"
},
"maintainer": {
"type": "string"
},
"manifest": {
"properties": {
"main": {
"properties": {
"Archiver-Version": {
"type": "string"
},
"Build-Jdk": {
"type": "string"
},
"Built-By": {
"type": "string"
},
"Created-By": {
"type": "string"
},
"Extension-Name": {
"type": "string"
},
"Group-Id": {
"type": "string"
},
"Hudson-Version": {
"type": "string"
},
"Implementation-Title": {
"type": "string"
},
"Implementation-Version": {
"type": "string"
},
"Jenkins-Version": {
"type": "string"
},
"Long-Name": {
"type": "string"
},
"Main-Class": {
"type": "string"
},
"Manifest-Version": {
"type": "string"
},
"Minimum-Java-Version": {
"type": "string"
},
"Plugin-Dependencies": {
"type": "string"
},
"Plugin-Developers": {
"type": "string"
},
"Plugin-License-Name": {
"type": "string"
},
"Plugin-License-Url": {
"type": "string"
},
"Plugin-ScmUrl": {
"type": "string"
},
"Plugin-Version": {
"type": "string"
},
"Short-Name": {
"type": "string"
},
"Specification-Title": {
"type": "string"
}
},
"required": [
"Archiver-Version",
"Build-Jdk",
"Built-By",
"Created-By",
"Manifest-Version"
],
"type": "object"
}
},
"required": [
"main"
],
"type": "object"
},
"name": {
"type": "string"
},
"originPackage": {
"type": "string"
},
"package": {
"type": "string"
},
"platform": {
"type": "string"
},
"pomProperties": {
"properties": {
"artifactId": {
"type": "string"
},
"extraFields": {
"type": "null"
},
"groupId": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"artifactId",
"extraFields",
"groupId",
"name",
"path",
"version"
],
"type": "object"
},
"pullChecksum": {
"type": "string"
},
"pullDependencies": {
"type": "string"
},
"release": {
"type": "string"
},
"sitePackagesRootPath": {
"type": "string"
},
"size": {
"type": "integer"
},
"source": {
"type": "string"
},
"sourceRpm": {
"type": "string"
},
"topLevelPackages": {
"items": {
"type": "string"
},
"type": "array"
},
"url": {
"type": "string"
},
"vendor": {
"type": "string"
},
"version": {
"type": "string"
},
"virtualPath": {
"type": "string"
}
},
"type": "object"
},
"metadataType": {
"type": "string"
},
"name": {
"type": "string"
},
"type": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"foundBy",
"language",
"licenses",
"locations",
"metadataType",
"name",
"type",
"version"
],
"type": "object"
},
"type": "array"
},
"descriptor": {
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"name",
"version"
],
"type": "object"
},
"distro": {
"properties": {
"idLike": {
"type": "string"
},
"name": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"idLike",
"name",
"version"
],
"type": "object"
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Package"
},
"type": "array"
},
"source": {
"properties": {
"target": {
"anyOf": [
{
"type": "string"
},
{
"properties": {
"digest": {
"type": "string"
},
"layers": {
"items": {
"properties": {
"digest": {
"type": "string"
},
"mediaType": {
"type": "string"
},
"size": {
"type": "integer"
}
},
"required": [
"digest",
"mediaType",
"size"
],
"type": "object"
},
"type": "array"
},
"mediaType": {
"type": "string"
},
"scope": {
"type": "string"
},
"size": {
"type": "integer"
},
"tags": {
"items": {
"type": "string"
},
"type": "array"
},
"userInput": {
"type": "string"
}
},
"required": [
"digest",
"layers",
"mediaType",
"scope",
"size",
"tags",
"userInput"
],
"type": "object"
}
]
},
"type": {
"type": "string"
}
},
"required": [
"target",
"type"
],
"type": "object"
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Source"
},
"distro": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Distribution"
},
"descriptor": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Descriptor"
}
},
"additionalProperties": false,
"type": "object"
},
"required": [
"artifacts",
"descriptor",
"distro",
"source"
],
"type": "object"
}
"Location": {
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
},
"layerID": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"Package": {
"required": [
"name",
"version",
"type",
"foundBy",
"locations",
"licenses",
"language",
"cpes",
"purl",
"metadataType"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"type": {
"type": "string"
},
"foundBy": {
"type": "string"
},
"locations": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Location"
},
"type": "array"
},
"licenses": {
"items": {
"type": "string"
},
"type": "array"
},
"language": {
"type": "string"
},
"cpes": {
"items": {
"type": "string"
},
"type": "array"
},
"purl": {
"type": "string"
},
"metadataType": {
"type": "string"
},
"metadata": {
"additionalProperties": true
}
},
"additionalProperties": false,
"type": "object"
},
"Source": {
"required": [
"type",
"target"
],
"properties": {
"type": {
"type": "string"
},
"target": {
"additionalProperties": true
}
},
"additionalProperties": false,
"type": "object"
}
}
}

View file

@ -24,7 +24,7 @@ type PackageJSON struct {
Latest []string `json:"latest"`
Author Author `json:"author"`
License json.RawMessage `json:"license"`
Licenses []license `json:"licenses,omitempty"`
Licenses []license `json:"licenses"`
Name string `json:"name"`
Homepage string `json:"homepage"`
Description string `json:"description"`

View file

@ -30,7 +30,7 @@ type packageBasicMetadata struct {
// packageCustomMetadata contains ambiguous values (type-wise) from pkg.Package.
type packageCustomMetadata struct {
MetadataType pkg.MetadataType `json:"metadataType"`
Metadata interface{} `json:"metadata,omitempty"`
Metadata interface{} `json:"metadata"`
}
// packageMetadataUnpacker is all values needed from Package to disambiguate ambiguous fields during json unmarshaling.
@ -45,14 +45,27 @@ func NewPackage(p *pkg.Package) (Package, error) {
for i, c := range p.CPEs {
cpes[i] = c.BindToFmtString()
}
// ensure collections are never nil for presentation reasons
var locations = make([]source.Location, 0)
if p.Locations != nil {
locations = p.Locations
}
var licenses = make([]string, 0)
if p.Licenses != nil {
licenses = p.Licenses
}
return Package{
packageBasicMetadata: packageBasicMetadata{
Name: p.Name,
Version: p.Version,
Type: p.Type,
FoundBy: p.FoundBy,
Locations: p.Locations,
Licenses: p.Licenses,
Locations: locations,
Licenses: licenses,
Language: p.Language,
CPEs: cpes,
PURL: p.PURL,

View file

@ -39,7 +39,7 @@
"path": "/some/path/pkg1"
}
],
"licenses": null,
"licenses": [],
"language": "",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"

View file

@ -41,7 +41,7 @@
"layerID": "sha256:da21056e7bf4308ecea0c0836848a7fe92f38fdcf35bc09ee6d98e7ab7beeebf"
}
],
"licenses": null,
"licenses": [],
"language": "",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"

View file

@ -82,6 +82,11 @@ func TestCatalogFromJSON(t *testing.T) {
}
for _, d := range deep.Equal(a, e) {
// ignore errors for empty collections vs nil for select fields
// TODO: this is brittle, but not dangerously so. We should still find a better way to do this.
if d == "Licenses: [] != <nil slice>" {
continue
}
t.Errorf(" package %d (name=%s) diff: %+v", i, e.Name, d)
}
}

View file

@ -3,7 +3,6 @@ package integration
import (
"bytes"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
@ -20,7 +19,6 @@ import (
)
const jsonSchemaPath = "schema/json"
const jsonSchemaExamplesPath = jsonSchemaPath + "/examples"
func repoRoot(t *testing.T) string {
t.Helper()
@ -54,11 +52,6 @@ func validateAgainstV1Schema(t *testing.T, json string) {
}
func testJsonSchema(t *testing.T, catalog *pkg.Catalog, theScope source.Source, prefix string) {
// make the json output example dir if it does not exist
absJsonSchemaExamplesPath := path.Join(repoRoot(t), jsonSchemaExamplesPath)
if _, err := os.Stat(absJsonSchemaExamplesPath); os.IsNotExist(err) {
os.Mkdir(absJsonSchemaExamplesPath, 0755)
}
output := bytes.NewBufferString("")
@ -77,21 +70,6 @@ func testJsonSchema(t *testing.T, catalog *pkg.Catalog, theScope source.Source,
t.Fatalf("unable to present: %+v", err)
}
// we use the examples dir as a way to use integration tests to drive what valid examples are in case we
// want to update the json schema. We do not want to validate the output of the presentation format as the
// contents may change regularly, making the integration tests brittle.
testFileName := prefix + "_" + path.Base(t.Name()) + ".json"
testFilePath := path.Join(absJsonSchemaExamplesPath, testFileName)
fh, err := os.OpenFile(testFilePath, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
t.Fatalf("unable to open json example path: %+v", err)
}
_, err = fh.WriteString(output.String())
if err != nil {
t.Fatalf("unable to write json example: %+v", err)
}
validateAgainstV1Schema(t, output.String())
}