-
Notifications
You must be signed in to change notification settings - Fork 45
feat(reconcile): add a MetaSchema kind for declarative metaschema management #1882
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
a1c2575
8c87c1e
32e4692
1ba48d4
b1dd75f
f1a63a3
6b7a621
3a752a2
04bb0d7
8e0dd53
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 |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package metaschema | ||
|
|
||
| import _ "embed" | ||
|
|
||
| // Built-in metaschema names. These are the schemas the server validates entity | ||
| // metadata against, and the set the MetaSchema reconcile kind manages. | ||
| const ( | ||
| NameUser = "user" | ||
| NameGroup = "group" | ||
| NameOrg = "organization" | ||
| NameRole = "role" | ||
| NameProspect = "prospect" | ||
| ) | ||
|
|
||
| //go:embed metaschemas/user.json | ||
| var defaultUser []byte | ||
|
|
||
| //go:embed metaschemas/group.json | ||
| var defaultGroup []byte | ||
|
|
||
| //go:embed metaschemas/org.json | ||
| var defaultOrg []byte | ||
|
|
||
| //go:embed metaschemas/role.json | ||
| var defaultRole []byte | ||
|
|
||
| //go:embed metaschemas/prospect.json | ||
| var defaultProspect []byte | ||
|
|
||
| // Defaults maps each built-in metaschema name to its shipped JSON schema. It is | ||
| // the one source for both the server seeding (MigrateDefaults) and the MetaSchema | ||
| // reconcile kind, so adding a metaschema for a new resource is a single edit here. | ||
| var Defaults = map[string]string{ | ||
| NameUser: string(defaultUser), | ||
| NameGroup: string(defaultGroup), | ||
| NameOrg: string(defaultOrg), | ||
| NameRole: string(defaultRole), | ||
| NameProspect: string(defaultProspect), | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package metaschema | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestDefaults(t *testing.T) { | ||
| want := []string{NameUser, NameGroup, NameOrg, NameRole, NameProspect} | ||
| if len(Defaults) != len(want) { | ||
| t.Fatalf("Defaults has %d entries, want %d", len(Defaults), len(want)) | ||
| } | ||
| for _, name := range want { | ||
| schema, ok := Defaults[name] | ||
| if !ok { | ||
| t.Errorf("Defaults is missing %q", name) | ||
| continue | ||
| } | ||
| if schema == "" { | ||
| t.Errorf("Defaults[%q] is empty", name) | ||
| } | ||
| var v any | ||
| if err := json.Unmarshal([]byte(schema), &v); err != nil { | ||
| t.Errorf("Defaults[%q] is not valid JSON: %v", name, err) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,209 @@ | ||||||||||||||||||||||
| package reconcile | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import ( | ||||||||||||||||||||||
| "encoding/json" | ||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||
| "io" | ||||||||||||||||||||||
| "sort" | ||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // KindMetaSchema is the desired-state document kind for entity metaschemas. | ||||||||||||||||||||||
| const KindMetaSchema = "MetaSchema" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // MetaSchemaSpec is one desired metaschema. Name is a built-in metaschema name | ||||||||||||||||||||||
| // the server knows; Schema is the JSON schema as a string. | ||||||||||||||||||||||
| type MetaSchemaSpec struct { | ||||||||||||||||||||||
| Name string `yaml:"name"` | ||||||||||||||||||||||
| Schema string `yaml:"schema"` | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // currentMetaSchema is a metaschema as it exists on the server. | ||||||||||||||||||||||
| type currentMetaSchema struct { | ||||||||||||||||||||||
| ID string | ||||||||||||||||||||||
| Name string | ||||||||||||||||||||||
| Schema string | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // metaSchemaOp is a single planned change. schema is the JSON to write; id is the | ||||||||||||||||||||||
| // server id for an update, empty when the metaschema must be created. fromDefault | ||||||||||||||||||||||
| // marks a reset, so the plan can say so. | ||||||||||||||||||||||
| type metaSchemaOp struct { | ||||||||||||||||||||||
| name string | ||||||||||||||||||||||
| id string | ||||||||||||||||||||||
| schema string | ||||||||||||||||||||||
| fromDefault bool | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func (o metaSchemaOp) String() string { | ||||||||||||||||||||||
| switch { | ||||||||||||||||||||||
| case o.id == "": | ||||||||||||||||||||||
| return fmt.Sprintf("create metaschema %s", o.name) | ||||||||||||||||||||||
| case o.fromDefault: | ||||||||||||||||||||||
| return fmt.Sprintf("reset metaschema %s to default", o.name) | ||||||||||||||||||||||
| default: | ||||||||||||||||||||||
| return fmt.Sprintf("set metaschema %s", o.name) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // decodeJSON parses a single JSON value, preserving number literals so a large | ||||||||||||||||||||||
| // or high-precision number is not rounded through float64. It rejects trailing | ||||||||||||||||||||||
| // data after the value, matching a strict single-document parse. | ||||||||||||||||||||||
| func decodeJSON(s string) (any, error) { | ||||||||||||||||||||||
| dec := json.NewDecoder(strings.NewReader(s)) | ||||||||||||||||||||||
| dec.UseNumber() | ||||||||||||||||||||||
| var v any | ||||||||||||||||||||||
| if err := dec.Decode(&v); err != nil { | ||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if _, err := dec.Token(); err != io.EOF { | ||||||||||||||||||||||
| return nil, fmt.Errorf("unexpected trailing data after JSON value") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return v, nil | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // canonicalJSON returns a stable form of a JSON document, so two schemas that | ||||||||||||||||||||||
| // differ only in whitespace or key order compare equal, while a real difference | ||||||||||||||||||||||
| // in a number is kept (numbers are compared by their literal, not by float64). | ||||||||||||||||||||||
| // It keeps the export round-trip stable and stops formatting from making a false | ||||||||||||||||||||||
| // diff. | ||||||||||||||||||||||
| func canonicalJSON(s string) (string, error) { | ||||||||||||||||||||||
| v, err := decodeJSON(s) | ||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| return "", err | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| b, err := json.Marshal(v) | ||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| return "", err | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return string(b), nil | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // validateSchemaDocument checks the string is a JSON object, not just any valid | ||||||||||||||||||||||
| // JSON. A non-object root (a number, string, boolean, or array) parses as JSON | ||||||||||||||||||||||
| // but is not a usable metadata schema: the server compiles it with gojsonschema | ||||||||||||||||||||||
| // for every entity of that type, and a non-object root errors there, which would | ||||||||||||||||||||||
| // start failing all metadata writes for that entity. Catching it here keeps the | ||||||||||||||||||||||
| // whole file from partially applying. | ||||||||||||||||||||||
| func validateSchemaDocument(schema string) error { | ||||||||||||||||||||||
| root, err := decodeJSON(schema) | ||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| return fmt.Errorf("schema is not valid JSON: %w", err) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if _, ok := root.(map[string]any); !ok { | ||||||||||||||||||||||
| return fmt.Errorf("schema must be a JSON object") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return nil | ||||||||||||||||||||||
|
Comment on lines
+88
to
+96
Contributor
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. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🌐 Web query:
💡 Result: For the github.com/xeipuuv/gojsonschema repository, the behavior for validating against the schema {"type": 123} is as follows: 1. Schema-compilation error: No, this does not trigger a schema-compilation error by default [1]. The library treats the schema as a valid JSON object. During validation, the "type" keyword expects a string or an array of strings (per JSON Schema specifications). Because 123 is a number, the library will fail to correctly process the "type" constraint during the validation phase, typically resulting in a validation result indicating the data does not match, rather than a hard failure during the NewSchema or Compile compilation step [1]. If you enable meta-schema validation via the SchemaLoader (setting sl.Validate = true), the library may detect the schema itself is invalid against the JSON Schema meta-schema, which would then return an error during the compilation/loading phase [1]. 2. Resolving remote $ref targets: No, the library does not provide a native configuration option to disable the automatic resolution of remote $ref targets [2]. The implementation automatically attempts to fetch remote references (e.g., via http/https) when encountered [3]. Users have reported that this behavior is "baked in" and difficult to bypass without external workarounds, such as pre-loading all required schemas using AddSchema/AddSchemas to satisfy dependencies before they are fetched over the network [2][3]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files | grep -E '(^|/)(metaschema\.go|service\.go|go\.mod|go\.sum)$' || true
printf '%s\n' '--- relevant symbols and calls ---'
rg -n -C 5 'validateSchemaDocument|gojsonschema|SchemaLoader|Validate|exportMetaSchemas|MetaSchemaReconciler' internal core go.mod go.sum 2>/dev/null || true
printf '%s\n' '--- targeted source excerpts ---'
for f in internal/reconcile/metaschema.go core/metaschema/service.go go.mod; do
if [ -f "$f" ]; then
echo "### $f"
nl -ba "$f" | sed -n '1,180p'
fi
doneRepository: raystack/frontier Length of output: 50374 🏁 Script executed: #!/bin/bash
set -eu
for f in internal/reconcile/metaschema.go internal/reconcile/metaschema_reconciler.go core/metaschema/service.go core/metaschema/metaschema.go internal/reconcile/metaschema_test.go internal/reconcile/metaschema_reconciler_test.go go.mod; do
if [ -f "$f" ]; then
echo "### $f"
wc -l "$f"
nl -ba "$f" | sed -n '1,220p'
fi
done
echo '### gojsonschema references'
rg -n -C 8 'gojsonschema|SchemaLoader|NewSchema|Validate\(' core internal --glob '*.go' | head -n 300
echo '### local module source availability'
go env GOMODCACHE GOPATH 2>/dev/null || true
find "${GOMODCACHE:-/nonexistent}" -maxdepth 5 -type d -path '*gojsonschema*' -print 2>/dev/null | head -n 20Repository: raystack/frontier Length of output: 26225 🏁 Script executed: #!/bin/bash
set -eu
for spec in \
'internal/reconcile/metaschema.go:1,210' \
'internal/reconcile/metaschema_reconciler.go:1,112' \
'core/metaschema/service.go:1,117' \
'core/metaschema/metaschema.go:1,24' \
'internal/reconcile/metaschema_test.go:1,251' \
'internal/reconcile/metaschema_reconciler_test.go:1,146'; do
file=${spec%:*}
range=${spec#*:}
echo "### $file"
sed -n "${range}p" "$file"
done
echo '### direct gojsonschema imports and calls'
rg -n -C 10 'github.com/xeipuuv/gojsonschema|gojsonschema\.' --glob '*.go' . || true
mod=/home/jailuser/go/pkg/mod/github.com/xeipuuv/gojsonschema@v1.2.0
echo "### gojsonschema v1.2.0 files"
find "$mod" -maxdepth 1 -type f -printf '%f\n' | sort
echo '### loader/compiler/validation implementation'
rg -n -C 12 'type SchemaLoader|Validate|NewSchema|Compile|validateSchema|typeSchema|remote|ReferenceLoader' "$mod" --glob '*.go' | head -n 500Repository: raystack/frontier Length of output: 50373 🏁 Script executed: #!/bin/bash
set -eu
mod=/home/jailuser/go/pkg/mod/github.com/xeipuuv/gojsonschema@v1.2.0
echo '### SchemaLoader and Validate defaults'
sed -n '1,240p' "$mod/schemaLoader.go"
sed -n '1,260p' "$mod/schema.go"
echo '### type keyword handling'
rg -n -C 15 'Type|type.*keyword|NewType|parseType|TypeError|expected.*type' "$mod" --glob '*.go' | head -n 300
echo '### remote reference loading'
sed -n '130,240p' "$mod/schemaPool.go"
rg -n -C 15 'func \(.*\) LoadJSON|http.Get|Get\(|NewStringLoader|NewSchemaLoader|Validate =' "$mod" --glob '*.go' | head -n 300
echo '### isolated runtime probe'
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
cat > "$tmp/go.mod" <<EOF
module isolated-probe
go 1.20
require github.com/xeipuuv/gojsonschema v1.2.0
EOF
cat > "$tmp/main.go" <<'EOF'
package main
import (
"fmt"
"github.com/xeipuuv/gojsonschema"
)
func main() {
schemas := []string{
`{"type":123}`,
`{"type":"object"}`,
`{"$ref":"https://127.0.0.1:1/unreachable.json"}`,
}
for _, raw := range schemas {
result, err := gojsonschema.Validate(
gojsonschema.NewStringLoader(raw),
gojsonschema.NewGoLoader(map[string]any{}),
)
fmt.Printf("schema=%s\nerr=%v\n", raw, err)
if result != nil {
fmt.Printf("valid=%v\n", result.Valid())
}
}
}
EOF
(
cd "$tmp"
GOPROXY=off GOSUMDB=off go run .
)Repository: raystack/frontier Length of output: 50373 🏁 Script executed: #!/bin/bash
set -u
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
cat > "$tmp/go.mod" <<'EOF'
module isolated-probe
go 1.20
require github.com/xeipuuv/gojsonschema v1.2.0
EOF
cat > "$tmp/main.go" <<'EOF'
package main
import (
"fmt"
"github.com/xeipuuv/gojsonschema"
)
func main() {
schemas := []string{
`{"type":123}`,
`{"type":["object",123]}`,
`{"required":123}`,
`{"properties":123}`,
`{"items":123}`,
`{"minimum":"x"}`,
`{"$ref":"https://127.0.0.1:1/unreachable.json"}`,
`{"type":"object"}`,
}
for _, raw := range schemas {
result, err := gojsonschema.Validate(
gojsonschema.NewStringLoader(raw),
gojsonschema.NewGoLoader(map[string]any{}),
)
fmt.Printf("schema=%s\nerr=%v\n", raw, err)
if result != nil {
fmt.Printf("valid=%v errors=%d\n", result.Valid(), len(result.Errors()))
}
}
}
EOF
cd "$tmp"
GOPROXY=off GOSUMDB=off GOFLAGS=-mod=mod go run . 2>&1Repository: raystack/frontier Length of output: 392 🏁 Script executed: #!/bin/bash
set -u
echo '### cached dependency versions'
find /home/jailuser/go/pkg/mod/github.com/xeipuuv -maxdepth 1 -mindepth 1 -type d -printf '%f\n' | sort
find /home/jailuser/go/pkg/mod/cache/download/github.com/xeipuuv -type f -name '*.mod' -o -name '*.ziphash' 2>/dev/null | sort | head -n 100
mod=/home/jailuser/go/pkg/mod/github.com/xeipuuv/gojsonschema@v1.2.0
echo '### NewStringLoader and loader factories'
rg -n -C 12 'func NewStringLoader|type stringJSONLoader|LoaderFactory|func \(.*\) LoaderFactory' "$mod" --glob '*.go'
echo '### exact malformed-keyword branches'
rg -n -C 20 'KEY_TYPE|KEY_REQUIRED|KEY_PROPERTIES|KEY_ITEMS|KEY_MINIMUM|KEY_MAXIMUM' "$mod/schema.go" | head -n 500
echo '### compile a local-replacement probe when cached modules permit it'
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
cat > "$tmp/go.mod" <<'EOF'
module isolated-probe
go 1.20
require (
github.com/xeipuuv/gojsonschema v1.2.0
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415
)
replace github.com/xeipuuv/gojsonschema => /home/jailuser/go/pkg/mod/github.com/xeipuuv/gojsonschema@v1.2.0
replace github.com/xeipuuv/gojsonpointer => /home/jailuser/go/pkg/mod/github.com/xeipuuv/gojsonpointer@v0.0.0-20190905194746-02993c407bfb
replace github.com/xeipuuv/gojsonreference => /home/jailuser/go/pkg/mod/github.com/xeipuuv/gojsonreference@v0.0.0-20180127040603-bd5ef7bd5415
EOF
cat > "$tmp/main.go" <<'EOF'
package main
import (
"fmt"
"github.com/xeipuuv/gojsonschema"
)
func main() {
for _, raw := range []string{
`{"type":123}`,
`{"type":["object",123]}`,
`{"required":123}`,
`{"properties":123}`,
`{"items":123}`,
`{"minimum":"x"}`,
`{"$ref":"https://127.0.0.1:1/unreachable.json"}`,
} {
result, err := gojsonschema.Validate(
gojsonschema.NewStringLoader(raw),
gojsonschema.NewGoLoader(map[string]any{}),
)
fmt.Printf("schema=%s\nerr=%v\n", raw, err)
if result != nil {
fmt.Printf("valid=%v errors=%d\n", result.Valid(), len(result.Errors()))
}
}
}
EOF
(cd "$tmp" && GOPROXY=off GOSUMDB=off GOFLAGS=-mod=mod go run .) 2>&1Repository: raystack/frontier Length of output: 44934 🏁 Script executed: #!/bin/bash
set -eu
mod=/home/jailuser/go/pkg/mod/github.com/xeipuuv/gojsonschema@v1.2.0
echo '### public Validate wrapper'
rg -n -C 20 '^func Validate|^func \(.*\) Validate' "$mod" --glob '*.go'
echo '### HTTP reference loader'
sed -n '132,225p' "$mod/jsonLoader.go"
echo '### reference parsing and remote resolution'
sed -n '48,125p' "$mod/schemaPool.go"
sed -n '180,225p' "$mod/schemaPool.go"Repository: raystack/frontier Length of output: 11302 Reject schemas that
|
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // validateMetaSchemaSpecs checks every entry without touching the server: the | ||||||||||||||||||||||
| // name is a known built-in, no name repeats, and the schema is a non-empty JSON | ||||||||||||||||||||||
| // object. defaults is the managed set and the source of known names. Names are | ||||||||||||||||||||||
| // matched case-insensitively, matching the sibling kinds, so `Organization` and | ||||||||||||||||||||||
| // `organization` both name the same built-in. | ||||||||||||||||||||||
| func validateMetaSchemaSpecs(specs []MetaSchemaSpec, defaults map[string]string) error { | ||||||||||||||||||||||
| seen := map[string]struct{}{} | ||||||||||||||||||||||
| for _, s := range specs { | ||||||||||||||||||||||
| name := strings.ToLower(strings.TrimSpace(s.Name)) | ||||||||||||||||||||||
| if name == "" { | ||||||||||||||||||||||
| return fmt.Errorf("metaschema name is required") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if _, ok := defaults[name]; !ok { | ||||||||||||||||||||||
|
rohilsurana marked this conversation as resolved.
|
||||||||||||||||||||||
| return fmt.Errorf("unknown metaschema %q", s.Name) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if _, dup := seen[name]; dup { | ||||||||||||||||||||||
| return fmt.Errorf("metaschema %q is listed more than once", name) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| seen[name] = struct{}{} | ||||||||||||||||||||||
| if strings.TrimSpace(s.Schema) == "" { | ||||||||||||||||||||||
| return fmt.Errorf("metaschema %q: schema is required", name) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if err := validateSchemaDocument(s.Schema); err != nil { | ||||||||||||||||||||||
| return fmt.Errorf("metaschema %q: %w", name, err) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return nil | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // diffMetaSchemas returns the ops that make the server's built-in metaschemas | ||||||||||||||||||||||
| // match the desired spec. The file is the full desired state: a built-in the file | ||||||||||||||||||||||
| // lists is set to its schema, and a built-in the file leaves out is reset to its | ||||||||||||||||||||||
| // shipped default. defaults is the managed set: its keys are the built-ins, its | ||||||||||||||||||||||
| // values the reset targets. | ||||||||||||||||||||||
| func diffMetaSchemas(desired []MetaSchemaSpec, current []currentMetaSchema, defaults map[string]string) ([]metaSchemaOp, error) { | ||||||||||||||||||||||
| desiredByName := make(map[string]string, len(desired)) | ||||||||||||||||||||||
| for _, s := range desired { | ||||||||||||||||||||||
| desiredByName[strings.ToLower(strings.TrimSpace(s.Name))] = s.Schema | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| currentByName := make(map[string]currentMetaSchema, len(current)) | ||||||||||||||||||||||
| for _, c := range current { | ||||||||||||||||||||||
| currentByName[c.Name] = c | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| names := make([]string, 0, len(defaults)) | ||||||||||||||||||||||
| for name := range defaults { | ||||||||||||||||||||||
| names = append(names, name) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| sort.Strings(names) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| var ops []metaSchemaOp | ||||||||||||||||||||||
| for _, name := range names { | ||||||||||||||||||||||
| want, inFile := desiredByName[name] | ||||||||||||||||||||||
| if !inFile { | ||||||||||||||||||||||
| want = defaults[name] | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| wantCanon, err := canonicalJSON(want) | ||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| return nil, fmt.Errorf("metaschema %q: schema is not valid JSON: %w", name, err) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| cur, exists := currentByName[name] | ||||||||||||||||||||||
| if exists { | ||||||||||||||||||||||
| if curCanon, err := canonicalJSON(cur.Schema); err == nil && curCanon == wantCanon { | ||||||||||||||||||||||
| continue | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| ops = append(ops, metaSchemaOp{ | ||||||||||||||||||||||
| name: name, | ||||||||||||||||||||||
| id: cur.ID, // empty when the metaschema is not on the server | ||||||||||||||||||||||
| schema: want, | ||||||||||||||||||||||
| fromDefault: !inFile, | ||||||||||||||||||||||
| }) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return ops, nil | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // exportMetaSchemas returns the built-ins whose current schema differs from the | ||||||||||||||||||||||
| // default, sorted by name, as a desired-state spec. A built-in at its default is | ||||||||||||||||||||||
| // omitted, so reconciling an export plans no changes. | ||||||||||||||||||||||
| func exportMetaSchemas(current []currentMetaSchema, defaults map[string]string) ([]MetaSchemaSpec, error) { | ||||||||||||||||||||||
| byName := make(map[string]currentMetaSchema, len(current)) | ||||||||||||||||||||||
| for _, c := range current { | ||||||||||||||||||||||
| byName[c.Name] = c | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| names := make([]string, 0, len(defaults)) | ||||||||||||||||||||||
| for name := range defaults { | ||||||||||||||||||||||
| names = append(names, name) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| sort.Strings(names) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| var specs []MetaSchemaSpec | ||||||||||||||||||||||
| for _, name := range names { | ||||||||||||||||||||||
| cur, exists := byName[name] | ||||||||||||||||||||||
| if !exists { | ||||||||||||||||||||||
| continue | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| curCanon, err := canonicalJSON(cur.Schema) | ||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| // A stored schema that is not valid JSON still round-trips as its own | ||||||||||||||||||||||
| // literal, so emit it rather than dropping it. | ||||||||||||||||||||||
| specs = append(specs, MetaSchemaSpec{Name: name, Schema: cur.Schema}) | ||||||||||||||||||||||
| continue | ||||||||||||||||||||||
|
Comment on lines
+196
to
+201
Contributor
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. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Do not export a spec that reconcile rejects. When a stored schema is invalid JSON, this branch emits it verbatim. Return an export error with the metaschema name for invalid stored JSON, or define an exported representation that validation accepts. Proposed fix curCanon, err := canonicalJSON(cur.Schema)
if err != nil {
- // A stored schema that is not valid JSON still round-trips as its own
- // literal, so emit it rather than dropping it.
- specs = append(specs, MetaSchemaSpec{Name: name, Schema: cur.Schema})
- continue
+ return nil, fmt.Errorf("metaschema %q contains invalid JSON: %w", name, err)
}📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if defCanon, err := canonicalJSON(defaults[name]); err == nil && defCanon == curCanon { | ||||||||||||||||||||||
| continue | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| specs = append(specs, MetaSchemaSpec{Name: name, Schema: cur.Schema}) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return specs, nil | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.