Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1,589 changes: 1,589 additions & 0 deletions blueprints/thousandeyes-kiota/resources/alert_rule.blueprint.json

Large diffs are not rendered by default.

871 changes: 871 additions & 0 deletions blueprints/thousandeyes-kiota/resources/dashboard.blueprint.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

108 changes: 107 additions & 1 deletion internal/docpatch/docpatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
return nil, nil
}
if err != nil {
return nil, err

Check failure on line 75 in internal/docpatch/docpatch.go

View workflow job for this annotation

GitHub Actions / ✨ Run golangci-lint

error returned from external package is unwrapped: sig: func os.ReadDir(name string) ([]os.DirEntry, error) (wrapcheck)
}

var patches []Patch
Expand All @@ -83,18 +83,18 @@
path := filepath.Join(dir, e.Name())
data, err := os.ReadFile(path) //nolint:gosec // enumerated from the operator-supplied dir
if err != nil {
return nil, err

Check failure on line 86 in internal/docpatch/docpatch.go

View workflow job for this annotation

GitHub Actions / ✨ Run golangci-lint

error returned from external package is unwrapped: sig: func os.ReadFile(name string) ([]byte, error) (wrapcheck)
}
var p Patch
if err := json.Unmarshal(data, &p); err != nil {
return nil, fmt.Errorf("%s is not a usable document patch: %w", path, err)
}
if p.Justification == "" {
return nil, fmt.Errorf("%s has no justification; a patch must name the evidence "+

Check failure on line 93 in internal/docpatch/docpatch.go

View workflow job for this annotation

GitHub Actions / ✨ Run golangci-lint

do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"%s has no justification; a patch must name the evidence \"+\n\t\"(a recording, a vendor statement) that proves the document wrong\", path)" (err113)
"(a recording, a vendor statement) that proves the document wrong", path)
}
if len(p.Operations) == 0 {
return nil, fmt.Errorf("%s declares no operations", path)

Check failure on line 97 in internal/docpatch/docpatch.go

View workflow job for this annotation

GitHub Actions / ✨ Run golangci-lint

do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"%s declares no operations\", path)" (err113)
}
p.File = path
patches = append(patches, p)
Expand All @@ -113,7 +113,7 @@
return nil, fmt.Errorf("the snapshot is not usable YAML: %w", err)
}
if root.Kind != yaml.DocumentNode || len(root.Content) == 0 {
return nil, fmt.Errorf("the snapshot is not a single YAML document")

Check failure on line 116 in internal/docpatch/docpatch.go

View workflow job for this annotation

GitHub Actions / ✨ Run golangci-lint

do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"the snapshot is not a single YAML document\")" (err113)
}

for _, p := range patches {
Expand All @@ -126,7 +126,7 @@

out, err := yaml.Marshal(&root)
if err != nil {
return nil, err

Check failure on line 129 in internal/docpatch/docpatch.go

View workflow job for this annotation

GitHub Actions / ✨ Run golangci-lint

error returned from external package is unwrapped: sig: func gopkg.in/yaml.v3.Marshal(in interface{}) (out []byte, err error) (wrapcheck)
}
return out, nil
}
Expand All @@ -134,9 +134,13 @@
// apply performs one operation against the document's top node.
func apply(top *yaml.Node, op Operation) error {
switch op.Op {
case "strip-schema-defaults":
// Not expressible in RFC 6902: every schema in the document loses its
// `default`, wherever schemas nest. Whole-document by definition.
return stripSchemaDefaults(top)
case "add", "replace", "remove", "test":
default:
return fmt.Errorf("unsupported op %q (add, replace, remove and test exist)", op.Op)
return fmt.Errorf("unsupported op %q (add, replace, remove, test and strip-schema-defaults exist)", op.Op)

Check failure on line 143 in internal/docpatch/docpatch.go

View workflow job for this annotation

GitHub Actions / ✨ Run golangci-lint

do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"unsupported op %q (add, replace, remove, test and strip-schema-defaults exist)\", op.Op)" (err113)
}

tokens, err := pointerTokens(op.Path)
Expand All @@ -144,7 +148,7 @@
return err
}
if len(tokens) == 0 {
return fmt.Errorf("the whole-document path %q is not patchable", op.Path)

Check failure on line 151 in internal/docpatch/docpatch.go

View workflow job for this annotation

GitHub Actions / ✨ Run golangci-lint

do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"the whole-document path %q is not patchable\", op.Path)" (err113)
}

parent, err := descend(top, tokens[:len(tokens)-1])
Expand All @@ -159,7 +163,7 @@
case yaml.SequenceNode:
return applyToSequence(parent, last, op)
default:
return fmt.Errorf("the parent of %q is neither an object nor an array", last)

Check failure on line 166 in internal/docpatch/docpatch.go

View workflow job for this annotation

GitHub Actions / ✨ Run golangci-lint

do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"the parent of %q is neither an object nor an array\", last)" (err113)
}
}

Expand All @@ -175,7 +179,7 @@
switch op.Op {
case "add", "replace":
if op.Op == "replace" && at < 0 {
return fmt.Errorf("nothing exists at %q to replace", key)

Check failure on line 182 in internal/docpatch/docpatch.go

View workflow job for this annotation

GitHub Actions / ✨ Run golangci-lint

do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"nothing exists at %q to replace\", key)" (err113)
}
if op.Op == "add" && at >= 0 && nodeEqual(node.Content[at+1], op.Value) {
return fmt.Errorf("the document already contains this value; the patch is stale -- " +
Expand Down Expand Up @@ -250,6 +254,108 @@
return nil
}

// stripSchemaDefaults removes the `default` key from every schema in the
// document: the named schemas under components, and every inline `schema`
// under paths. It walks the schema grammar rather than matching keys blindly,
// because "default" is also a legitimate property *name* and a legitimate key
// inside an example -- both of which must survive.
//
// Exists because a Kiota constructor stamps every spec-declared default onto
// the model it builds. On a request model, defaults on fields the provider
// never wires leak into every create and update body -- and an API that
// treats an absent field differently from its documented default then refuses
// bodies the practitioner never wrote. On a response model, the default masks
// absence: the getter answers the default where the wire said nothing. A
// wire-faithful provider needs neither, in either direction.
func stripSchemaDefaults(top *yaml.Node) error {
stripped := 0

if schemas := childValue(childValue(top, "components"), "schemas"); schemas != nil {
for i := 1; i < len(schemas.Content); i += 2 {
stripFromSchema(schemas.Content[i], &stripped)
}
}
if paths := childValue(top, "paths"); paths != nil {
stripUnderPaths(paths, &stripped)
}

if stripped == 0 {
return fmt.Errorf("the document declares no schema defaults; the patch is stale -- delete it")
}
return nil
}

// stripFromSchema removes `default` from one schema node and recurses into
// the positions of its grammar that hold further schemas.
func stripFromSchema(schema *yaml.Node, stripped *int) {
if schema == nil || schema.Kind != yaml.MappingNode {
return
}

for i := 0; i+1 < len(schema.Content); i += 2 {
if schema.Content[i].Value == "default" {
schema.Content = append(schema.Content[:i], schema.Content[i+2:]...)
*stripped++
break
}
}

for i := 0; i+1 < len(schema.Content); i += 2 {
key, value := schema.Content[i].Value, schema.Content[i+1]
switch key {
case "properties", "patternProperties":
// A map of property NAME to schema: the names are data (one may
// literally be "default"), the values are schemas.
for j := 1; j < len(value.Content); j += 2 {
stripFromSchema(value.Content[j], stripped)
}
case "items", "additionalProperties", "not":
stripFromSchema(value, stripped)
case "allOf", "anyOf", "oneOf":
for _, member := range value.Content {
stripFromSchema(member, stripped)
}
}
}
}

// stripUnderPaths finds every `schema` key beneath paths -- request bodies,
// responses, parameters -- and strips its value as a schema. Examples are
// data, not schemas, and are not entered.
func stripUnderPaths(node *yaml.Node, stripped *int) {
switch node.Kind {
case yaml.MappingNode:
for i := 0; i+1 < len(node.Content); i += 2 {
key, value := node.Content[i].Value, node.Content[i+1]
switch key {
case "schema":
stripFromSchema(value, stripped)
case "example", "examples":
continue
default:
stripUnderPaths(value, stripped)
}
}
case yaml.SequenceNode:
for _, member := range node.Content {
stripUnderPaths(member, stripped)
}
}
}

// childValue returns the value node of a mapping entry, or nil.
func childValue(node *yaml.Node, key string) *yaml.Node {
if node == nil || node.Kind != yaml.MappingNode {
return nil
}
for i := 0; i+1 < len(node.Content); i += 2 {
if node.Content[i].Value == key {
return node.Content[i+1]
}
}
return nil
}

// descend walks the node tree to the node a pointer prefix names.
func descend(node *yaml.Node, tokens []string) (*yaml.Node, error) {
for _, t := range tokens {
Expand Down
80 changes: 80 additions & 0 deletions internal/docpatch/docpatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,83 @@ func TestUnit_SpecPatch_LoadToleratesAMissingDirectory(t *testing.T) {
t.Errorf("a missing patches directory is simply no patches, got %v, %v", patches, err)
}
}

func TestUnit_DocPatch_StripSchemaDefaults(t *testing.T) {
t.Parallel()

doc := `
openapi: 3.0.1
paths:
/things:
post:
requestBody:
content:
application/json:
schema:
type: object
properties:
inline:
type: boolean
default: true
example:
default: kept-in-example
components:
schemas:
Thing:
type: object
default: whole-schema-default
properties:
enabled:
type: boolean
default: true
default:
type: string
description: a property literally named default, which must survive
allOf:
- type: object
properties:
nested:
type: integer
default: 7
items:
type: string
default: x
`
out, err := Apply([]byte(doc), []Patch{{
Justification: "live 400",
Operations: []Operation{{Op: "strip-schema-defaults", Path: "/"}},
}})
if err != nil {
t.Fatalf("Apply: %v", err)
}

var decoded map[string]any
if err := yaml.Unmarshal(out, &decoded); err != nil {
t.Fatal(err)
}
text := string(out)
for _, gone := range []string{"default: true", "default: 7", "default: x", "default: whole-schema-default"} {
if strings.Contains(text, gone) {
t.Errorf("a schema default survived: %q", gone)
}
}
if !strings.Contains(text, "a property literally named default") {
t.Error("the property named default was lost")
}
if !strings.Contains(text, "kept-in-example") {
t.Error("an example's default key was stripped; examples are data")
}
}

func TestUnit_DocPatch_StripSchemaDefaultsRefusesWhenNoneExist(t *testing.T) {
t.Parallel()

doc := "openapi: 3.0.1\ncomponents:\n schemas:\n Thing:\n type: object\n"
_, err := Apply([]byte(doc), []Patch{{
Justification: "live 400",
Operations: []Operation{{Op: "strip-schema-defaults", Path: "/"}},
}})
if err == nil || !strings.Contains(err.Error(), "stale") {
t.Errorf("a document with no defaults must refuse the strip as stale, got: %v", err)
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading