Files
cozystack/internal/apigate/model.go
Timofei Larkin 985f17b7be fix(ci): catch templated CRDs and APIService-only groups in the API gate
Shadow-observation of the merged gate found a real miss: a CRD shipped
inside a chart's templates/ dir (guarded by a Helm {{- if }}) was
invisible because discovery was gated on directory name (crds/,
definitions/, manifests/), and a group served only by an external
aggregated apiserver (kind: APIService, no CRD, no vendored Go types)
had no representation at all.

Switch CRD/APIService discovery from directory-name to content
sniffing, and tolerate Helm directive lines sharing a document with
otherwise-valid YAML (the closing `{{- end }}` of a wrapping
conditional has no leading `---`, so it lands in the same document as
the manifest). Add a fourth source, SourceAPIService, reusing the
existing empty-schema mechanism so a first-party group's first
APIService appearance requires review without pretending to diff a
schema that lives outside this repository.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-07-06 15:48:07 +03:00

132 lines
5.1 KiB
Go

/*
Copyright 2026 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package apigate detects "sizeable" Cozystack API changes between two
// checkouts of the repository so CI can require a review from a designated
// API owner. Sizeable means one of: a new API group, a new resource, or a
// breaking change to an existing resource's schema. The detection is fully
// deterministic — it parses the checked-in, codegen-kept-in-sync artifacts
// that define the served API surface, with no network or LLM calls.
package apigate
// Category classifies why a change is sizeable. The three values mirror the
// gate's contract exactly.
type Category string
const (
// NewGroup is emitted when an API group appears in head but not base.
NewGroup Category = "new-api-group"
// NewResource is emitted when a (group, resource) pair appears in head
// but not base.
NewResource Category = "new-resource"
// Breaking is emitted when an existing resource's schema changes in a
// way that can reject requests or objects that the base schema accepted
// (removed field, narrowed type/enum, newly-required field, tightened
// constraint, removed served version).
Breaking Category = "breaking-change"
// RemovedGroup is emitted when an entire API group present in base is gone
// from head. Deleting a group withdraws its whole API surface.
RemovedGroup Category = "removed-api-group"
// RemovedResource is emitted when a (group, resource) present in base is
// gone from head — the most disruptive single-resource change, since every
// stored object and client of that resource breaks.
RemovedResource Category = "removed-resource"
)
// Source identifies which checked-in artifact a resource was parsed from.
// It is carried through to findings so the report points reviewers at the
// right file family.
type Source string
const (
// SourceCRD is a CustomResourceDefinition YAML manifest.
SourceCRD Source = "crd"
// SourceCozyRD is an ApplicationDefinition (cozyrd) backing the
// apps.cozystack.io aggregated API.
SourceCozyRD Source = "cozyrd"
// SourceAPIServer is a static Go-backed aggregated resource discovered
// from the apiserver storage registrations. These carry no checked-in
// schema, so they participate only in new-group / new-resource
// detection.
SourceAPIServer Source = "apiserver"
// SourceAPIService is a group/version registered via a kind: APIService
// manifest, delegating to an aggregated apiserver that may be built
// entirely outside this repository (no vendored Go types, no CRD). Like
// SourceAPIServer, these carry no checked-in schema, so they participate
// only in new-group / new-resource / removal detection.
SourceAPIService Source = "apiservice"
)
// Resource is the normalized identity + schema of one API resource, keyed
// across base and head by (Group, Plural). Plural is always present; Kind may
// be empty for static Go-backed resources whose kind is not recoverable from
// the storage registration alone.
type Resource struct {
Group string
Kind string
Plural string
Source Source
// Origin is the repo-relative path the resource was parsed from, used
// only for human-facing reporting.
Origin string
// Versions maps a served version name to its parsed schema. Empty for
// SourceAPIServer resources (schema lives only in Go structs, out of
// scope for breaking detection).
Versions map[string]Schema
}
// key is the base/head correlation key for a resource.
func (r Resource) key() resourceKey { return resourceKey{Group: r.Group, Plural: r.Plural} }
type resourceKey struct {
Group string
Plural string
}
// Schema is a parsed JSON/OpenAPI-v3 schema node, using the generic
// map[string]any shape produced by sigs.k8s.io/yaml (JSON number/string/bool
// semantics). Comparing parsed nodes rather than raw text makes the diff
// insensitive to key ordering and formatting, and lets us ignore purely
// descriptive fields.
type Schema map[string]any
// Snapshot is the full set of API resources discovered in one checkout,
// indexed by correlation key.
type Snapshot map[resourceKey]Resource
// groups returns the set of API group names present in the snapshot.
func (s Snapshot) groups() map[string]struct{} {
out := make(map[string]struct{})
for k := range s {
out[k.Group] = struct{}{}
}
return out
}
// Finding is one reason a change is sizeable, ready to render into the CI
// report.
type Finding struct {
Category Category
Group string
Kind string
Plural string
Source Source
Origin string
// Detail is a human-readable explanation. For Breaking findings it names
// the schema path and the nature of the break.
Detail string
}