From b93e76e5cb39e54f286d66832a365f944795f5d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Mar 2026 15:17:07 +0100 Subject: [PATCH 1/2] [docs] Add provider compatibility matrix Add a Go tool (hack/generate-provider-matrix.go) that produces a Markdown compatibility matrix showing which core API types each provider implements. How it works: - Uses reflect.Type.Implements() to test each provider against the provider.*Provider interfaces defined in internal/provider/ - Self-validates by parsing Go AST of the provider package to ensure the hardcoded interfaceMap stays in sync with source interfaces - Discovers provider-specific types (config extensions and standalone CRDs) by scanning provider API directories for Register*Dependency calls and SchemeBuilder.Register calls - Resolves interface names to Kubernetes Kind names by correlating core _types.go filenames with Register*Dependency function names, then validating against the runtime scheme - Writes docs/provider-compatibility.md (full matrix) and updates a summary table in README.md between sentinel markers The new `make provider-matrix` target is also wired as a dependency of `make docs`, so the matrix is always regenerated with the API reference docs. --- .gitignore | 3 + Makefile | 9 +- Makefile.maker.yaml | 7 +- README.md | 14 + docs/provider-compatibility.md | 84 ++++ hack/generate-provider-matrix.go | 708 +++++++++++++++++++++++++++++++ 6 files changed, 822 insertions(+), 3 deletions(-) create mode 100644 docs/provider-compatibility.md create mode 100644 hack/generate-provider-matrix.go diff --git a/.gitignore b/.gitignore index e021a935f..be4127124 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ go.work.sum # Generated build-installer dist/* +# Generated provider matrix tool +generate-provider-matrix + # Kubernetes Generated files - skip generated files, except for vendored files !vendor/**/zz_generated.* diff --git a/Makefile b/Makefile index 602784246..54f3a75e3 100644 --- a/Makefile +++ b/Makefile @@ -180,7 +180,7 @@ run-docs: @docker build -t $(DOCS_IMG) -f docs/Dockerfile docs --load @docker run --rm --init -p 5173:5173 -v $(ROOT_DIR)/docs:/workspace -v /workspace/node_modules $(DOCS_IMG) -docs: install-crd-ref-docs +docs: install-crd-ref-docs provider-matrix crd-ref-docs --source-path=./api --config=./hack/api-reference/config.yaml --renderer=markdown --output-path=./docs/api-reference/index.md @$(SED) -i \ -e 's/#networkingmetalironcoredevv1alpha1/#networking-metal-ironcore-dev-v1alpha1/g' \ @@ -189,6 +189,11 @@ docs: install-crd-ref-docs -e 's/#xrcisconetworkingmetalironcoredevv1alpha1/#xr-cisco-networking-metal-ironcore-dev-v1alpha1/g' \ docs/api-reference/index.md +# Generate provider implementation matrix +provider-matrix: FORCE + @printf "\e[1;36m>> go run ./hack/generate-provider-matrix.go > docs/provider-compatibility.md\e[0m\n" + @go run ./hack/generate-provider-matrix.go > docs/provider-compatibility.md + install-goimports: FORCE @if ! hash goimports 2>/dev/null; then printf "\e[1;36m>> Installing goimports (this may take a while)...\e[0m\n"; go install golang.org/x/tools/cmd/goimports@latest; fi @@ -310,7 +315,7 @@ license-headers: FORCE install-addlicense @printf "\e[1;36m>> addlicense (for license headers on source code files)\e[0m\n" @printf "%s\0" $(patsubst $(shell awk '$$1 == "module" {print $$2}' go.mod)%,.%/*.go,$(shell go list ./...)) | $(XARGS) -0 -I{} bash -c 'year="$$(grep 'Copyright' {} | head -n1 | grep -E -o '"'"'[0-9]{4}(-[0-9]{4})?'"'"')"; if [[ -z "$$year" ]]; then year=$$(date +%Y); fi; gawk -i inplace '"'"'{if (display) {print} else {!/^\/\*/ && !/^\*/}}; {if (!display && $$0 ~ /^(package |$$)/) {display=1} else { }}'"'"' {}; addlicense -c "SAP SE or an SAP affiliate company and IronCore contributors" -s=only -y "$$year" -- {}; $(SED) -i '"'"'1s+// Copyright +// SPDX-FileCopyrightText: +'"'"' {}; ' @printf "\e[1;36m>> reuse annotate (for license headers on other files)\e[0m\n" - @reuse lint -j | jq -r '.non_compliant.missing_licensing_info[]' | grep -vw vendor | $(XARGS) reuse annotate -c 'SAP SE or an SAP affiliate company and IronCore contributors' -l Apache-2.0 --skip-unrecognised + @reuse lint -j | jq -r '.non_compliant.missing_licensing_info[]' | sed '/\/d' | $(XARGS) reuse annotate -c 'SAP SE or an SAP affiliate company and IronCore contributors' -l Apache-2.0 --skip-unrecognised @printf "\e[1;36m>> reuse download --all\e[0m\n" @reuse download --all @printf "\e[1;35mPlease review the changes. If *.license files were generated, consider instructing go-makefile-maker to add overrides to REUSE.toml instead.\e[0m\n" diff --git a/Makefile.maker.yaml b/Makefile.maker.yaml index 5b32c085f..f32804522 100644 --- a/Makefile.maker.yaml +++ b/Makefile.maker.yaml @@ -224,7 +224,7 @@ verbatim: | @docker build -t $(DOCS_IMG) -f docs/Dockerfile docs --load @docker run --rm --init -p 5173:5173 -v $(ROOT_DIR)/docs:/workspace -v /workspace/node_modules $(DOCS_IMG) - docs: install-crd-ref-docs + docs: install-crd-ref-docs provider-matrix crd-ref-docs --source-path=./api --config=./hack/api-reference/config.yaml --renderer=markdown --output-path=./docs/api-reference/index.md @$(SED) -i \ -e 's/#networkingmetalironcoredevv1alpha1/#networking-metal-ironcore-dev-v1alpha1/g' \ @@ -232,3 +232,8 @@ verbatim: | -e 's/#xecisconetworkingmetalironcoredevv1alpha1/#xe-cisco-networking-metal-ironcore-dev-v1alpha1/g' \ -e 's/#xrcisconetworkingmetalironcoredevv1alpha1/#xr-cisco-networking-metal-ironcore-dev-v1alpha1/g' \ docs/api-reference/index.md + + # Generate provider implementation matrix + provider-matrix: FORCE + @printf "\e[1;36m>> go run ./hack/generate-provider-matrix.go > docs/provider-compatibility.md\e[0m\n" + @go run ./hack/generate-provider-matrix.go > docs/provider-compatibility.md diff --git a/README.md b/README.md index ef21b7da0..23229cc9c 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,20 @@ Users can just run kubectl apply -f to install the project kubectl apply -f https://raw.githubusercontent.com//network-operator//dist/install.yaml ``` +## Supported Providers + +network-operator supports multiple network device providers with varying levels of feature support: + + +| Provider | Supported API Types | +|----------|---------------------| +| Cisco NX-OS | 22 / 22 | +| Cisco IOS-XR | 1 / 22 | +| OpenConfig | 1 / 22 | + + +For a detailed compatibility matrix showing which API types are supported by each provider, see [Provider Compatibility Matrix](docs/provider-compatibility.md). + ## Support, Feedback, Contributing This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/ironcore-dev/network-operator/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md). diff --git a/docs/provider-compatibility.md b/docs/provider-compatibility.md new file mode 100644 index 000000000..be0a7f261 --- /dev/null +++ b/docs/provider-compatibility.md @@ -0,0 +1,84 @@ + + +# Provider Compatibility Matrix + +This document provides a detailed overview of which API types are supported by each network device provider. + + + +## Compatibility Matrix + +| Core Kind | Cisco NX-OS | Cisco IOS-XR | OpenConfig | +|-----------|--------|--------|--------| +| `AccessControlList` | ✅ | — | — | +| `BGPPeer` | ✅ | — | — | +| `BGP` | ✅ | — | — | +| `Banner` | ✅ | — | — | +| `Certificate` | ✅ | — | — | +| `DNS` | ✅ | — | — | +| `EVPNInstance` | ✅ | — | — | +| `ISIS` | ✅ | — | — | +| `Interface` | ✅ | ✅ | ✅ | +| `LLDP` | ✅ | — | — | +| `ManagementAccess` | ✅ | — | — | +| `NTP` | ✅ | — | — | +| `NetworkVirtualizationEdge` | ✅ | — | — | +| `OSPF` | ✅ | — | — | +| `PIM` | ✅ | — | — | +| `PrefixSet` | ✅ | — | — | +| `RoutingPolicy` | ✅ | — | — | +| `SNMP` | ✅ | — | — | +| `Syslog` | ✅ | — | — | +| `User` | ✅ | — | — | +| `VLAN` | ✅ | — | — | +| `VRF` | ✅ | — | — | + +**Legend:** +- ✅ Supported +- — Not supported + +## Cisco NX-OS + +Cisco NX-OS provides comprehensive support for network configuration through the network-operator. + +**Core API types:** 22 / 22 + +**Provider-specific types:** + +| Kind | Category | +|------|----------| +| `BGPConfig` | Extends core type `BGP` | +| `InterfaceConfig` | Extends core type `Interface` | +| `LLDPConfig` | Extends core type `LLDP` | +| `ManagementAccessConfig` | Extends core type `ManagementAccess` | +| `NetworkVirtualizationEdgeConfig` | Extends core type `NetworkVirtualizationEdge` | +| `BorderGateway` | Provider-exclusive | +| `System` | Provider-exclusive | +| `VPCDomain` | Provider-exclusive | + +## Cisco IOS-XR + +Cisco IOS-XR support is currently in early development. + +**Core API types:** 1 / 22 + +## OpenConfig + +OpenConfig provides a vendor-neutral configuration interface using standard OpenConfig YANG models. + +**Core API types:** 1 / 22 + +## Contributing + +To add support for a new API type or provider: + +1. Define the provider interface in `internal/provider/` +2. Implement the interface methods in your provider package +3. Run `make provider-matrix` to regenerate this document +4. Submit a pull request with your changes + +The matrix is automatically generated by checking which provider interfaces +each provider implements, ensuring accuracy and eliminating manual maintenance. diff --git a/hack/generate-provider-matrix.go b/hack/generate-provider-matrix.go new file mode 100644 index 000000000..163b0c42e --- /dev/null +++ b/hack/generate-provider-matrix.go @@ -0,0 +1,708 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "errors" + "fmt" + "go/ast" + "go/parser" + "go/token" + "maps" + "os" + "path/filepath" + "reflect" + "slices" + "strings" + + "k8s.io/apimachinery/pkg/runtime" + + v1alpha1 "github.com/ironcore-dev/network-operator/api/core/v1alpha1" + "github.com/ironcore-dev/network-operator/internal/provider" + "github.com/ironcore-dev/network-operator/internal/provider/cisco/iosxr" + "github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos" + "github.com/ironcore-dev/network-operator/internal/provider/openconfig" +) + +// interfaceMap maps provider interface names to their reflect.Type. +// generate-provider-matrix fails if it finds a different set of Provider interfaces in the source code. +var interfaceMap = map[string]reflect.Type{ + "ACLProvider": reflect.TypeFor[provider.ACLProvider](), + "BannerProvider": reflect.TypeFor[provider.BannerProvider](), + "BGPProvider": reflect.TypeFor[provider.BGPProvider](), + "BGPPeerProvider": reflect.TypeFor[provider.BGPPeerProvider](), + "CertificateProvider": reflect.TypeFor[provider.CertificateProvider](), + "DNSProvider": reflect.TypeFor[provider.DNSProvider](), + "EVPNInstanceProvider": reflect.TypeFor[provider.EVPNInstanceProvider](), + "InterfaceProvider": reflect.TypeFor[provider.InterfaceProvider](), + "ISISProvider": reflect.TypeFor[provider.ISISProvider](), + "LLDPProvider": reflect.TypeFor[provider.LLDPProvider](), + "ManagementAccessProvider": reflect.TypeFor[provider.ManagementAccessProvider](), + "NTPProvider": reflect.TypeFor[provider.NTPProvider](), + "NVEProvider": reflect.TypeFor[provider.NVEProvider](), + "OSPFProvider": reflect.TypeFor[provider.OSPFProvider](), + "PIMProvider": reflect.TypeFor[provider.PIMProvider](), + "PrefixSetProvider": reflect.TypeFor[provider.PrefixSetProvider](), + "RoutingPolicyProvider": reflect.TypeFor[provider.RoutingPolicyProvider](), + "SNMPProvider": reflect.TypeFor[provider.SNMPProvider](), + "SyslogProvider": reflect.TypeFor[provider.SyslogProvider](), + "UserProvider": reflect.TypeFor[provider.UserProvider](), + "VLANProvider": reflect.TypeFor[provider.VLANProvider](), + "VRFProvider": reflect.TypeFor[provider.VRFProvider](), +} + +type providerInfo struct { + // name is the short identifier used as map key (e.g., "NXOS") + name string + // displayName is the human-readable name for documentation (e.g., "Cisco NX-OS") + displayName string + // description is shown in the provider section of the generated docs + description string + // apiDir is the path to the provider's API directory (e.g., "./api/cisco/nx/v1alpha1") + // Used to scan for provider-specific config configurations. + apiDir string + // instance is the provider implementation to check for interface support + instance provider.Provider +} + +// providers lists all registered providers to check for interface implementations. +// When adding a new provider, add it here and the matrix will automatically detect +// which interfaces it implements. +var providers = []providerInfo{ + { + name: "NXOS", + displayName: "Cisco NX-OS", + description: "Cisco NX-OS provides comprehensive support for network configuration through the network-operator.", + apiDir: "api/cisco/nx/v1alpha1", + instance: nxos.NewProvider(), + }, + { + name: "IOS-XR", + displayName: "Cisco IOS-XR", + description: "Cisco IOS-XR support is currently in early development.", + apiDir: "api/cisco/xr/v1alpha1", + instance: iosxr.NewProvider(), + }, + { + name: "OpenConfig", + displayName: "OpenConfig", + description: "OpenConfig provides a vendor-neutral configuration interface using standard OpenConfig YANG models.", + apiDir: "", // OpenConfig has no provider-specific API configurations + instance: openconfig.NewProvider(), + }, +} + +const ( + readmeBeginMarker = "" + readmeEndMarker = "" + + coreAPIDir = "api/core/v1alpha1" + dependencyPrefix = "Register" + dependencySuffix = "Dependency" +) + +// baseInterfaces are provider interfaces that don't map to a specific API type +// and should be excluded from the compatibility matrix. +var baseInterfaces = map[string]bool{ + "Provider": true, + "ProvisioningProvider": true, + "DeviceProvider": true, +} + +func main() { + // Verify interfaceMap has all entries by checking source code before generating + if err := verifyInterfaceMapComplete(); err != nil { + fmt.Fprintf(os.Stderr, "Consistency check failed: %v\n", err) + os.Exit(1) + } + + interfaceNames := slices.Sorted(maps.Keys(interfaceMap)) + + // Check implementations for each provider + implementations := make(map[string]map[string]bool) + counts := make(map[string]int) + configurations := make(map[string][]providerSpecificType) + standaloneTypes := make(map[string][]string) + + for _, prov := range providers { + implementations[prov.name] = make(map[string]bool) + providerType := reflect.TypeOf(prov.instance) + + for _, ifaceName := range interfaceNames { + interfaceType := interfaceMap[ifaceName] + implements := providerType.Implements(interfaceType) + implementations[prov.name][ifaceName] = implements + + if implements { + counts[prov.name]++ + } + } + + exts, err := scanConfigExtensions(prov.apiDir) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to scan config configurations for %s: %v\n", prov.name, err) + } + configurations[prov.name] = exts + + standalone, err := scanStandaloneTypes(prov.apiDir) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to scan standalone types for %s: %v\n", prov.name, err) + } + standaloneTypes[prov.name] = standalone + } + + // Build mapping from Register*Dependency core types to provider interface names + coreInfo, err := buildCoreTypeMapping() + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to build core type mapping: %v\n", err) + coreInfo = &coreTypeInfo{ + coreTypeToInterface: make(map[string]string), + interfaceToKind: make(map[string]string), + } + } + + // Generate markdown documentation + fmt.Println("") + fmt.Println() + fmt.Println("# Provider Compatibility Matrix") + fmt.Println() + fmt.Println("This document provides a detailed overview of which API types are supported by each network device provider.") + fmt.Println() + fmt.Println("") + fmt.Println() + + fmt.Println("## Compatibility Matrix") + fmt.Println() + + fmt.Print("| Core Kind |") + for _, prov := range providers { + fmt.Printf(" %s |", prov.displayName) + } + fmt.Println() + + fmt.Print("|-----------|") + for range providers { + fmt.Print("--------|") + } + fmt.Println() + + for _, ifaceName := range interfaceNames { + kindName := formatDisplayName(ifaceName) + if k, ok := coreInfo.interfaceToKind[ifaceName]; ok { + kindName = k + } + fmt.Printf("| `%s` |", kindName) + + for _, prov := range providers { + if implementations[prov.name][ifaceName] { + fmt.Print(" ✅ |") + } else { + fmt.Print(" — |") + } + } + fmt.Println() + } + + fmt.Println() + fmt.Println("**Legend:**") + fmt.Println("- ✅ Supported") + fmt.Println("- — Not supported") + fmt.Println() + + // Generate per-provider detail sections + total := len(interfaceMap) + for _, prov := range providers { + fmt.Printf("## %s\n", prov.displayName) + fmt.Println() + fmt.Println(prov.description) + fmt.Println() + fmt.Printf("**Core API types:** %d / %d\n", counts[prov.name], total) + if len(configurations[prov.name]) > 0 || len(standaloneTypes[prov.name]) > 0 { + fmt.Println() + fmt.Println("**Provider-specific types:**") + fmt.Println() + fmt.Println("| Kind | Category |") + fmt.Println("|------|----------|") + for _, ext := range configurations[prov.name] { + displayName := ext.coreType + if ifaceName, ok := coreInfo.coreTypeToInterface[ext.coreType]; ok { + if k, ok := coreInfo.interfaceToKind[ifaceName]; ok { + displayName = k + } else { + displayName = formatDisplayName(ifaceName) + } + } + fmt.Printf("| `%s` | Extends core type `%s` |\n", ext.kind, displayName) + } + for _, kind := range standaloneTypes[prov.name] { + fmt.Printf("| `%s` | Provider-exclusive |\n", kind) + } + } + fmt.Println() + } + + fmt.Println("## Contributing") + fmt.Println() + fmt.Println("To add support for a new API type or provider:") + fmt.Println() + fmt.Println("1. Define the provider interface in `internal/provider/`") + fmt.Println("2. Implement the interface methods in your provider package") + fmt.Println("3. Run `make provider-matrix` to regenerate this document") + fmt.Println("4. Submit a pull request with your changes") + fmt.Println() + fmt.Println("The matrix is automatically generated by checking which provider interfaces") + fmt.Println("each provider implements, ensuring accuracy and eliminating manual maintenance.") + + if err := updateReadmeSummary(counts); err != nil { + fmt.Fprintf(os.Stderr, "Failed to update README.md: %v\n", err) + os.Exit(1) + } +} + +func updateReadmeSummary(counts map[string]int) error { + data, err := os.ReadFile("README.md") + if err != nil { + return fmt.Errorf("reading README.md: %w", err) + } + + content := string(data) + beginIdx := strings.Index(content, readmeBeginMarker) + endIdx := strings.Index(content, readmeEndMarker) + + if beginIdx == -1 || endIdx == -1 { + return fmt.Errorf("markers %q and %q not found in README.md", readmeBeginMarker, readmeEndMarker) + } + + if endIdx <= beginIdx { + return errors.New("end marker appears before begin marker in README.md") + } + + // Build the new summary table + total := len(interfaceMap) + var summary strings.Builder + summary.WriteString(readmeBeginMarker + "\n") + summary.WriteString("| Provider | Supported API Types |\n") + summary.WriteString("|----------|---------------------|\n") + for _, prov := range providers { + fmt.Fprintf(&summary, "| %s | %d / %d |\n", prov.displayName, counts[prov.name], total) + } + summary.WriteString(readmeEndMarker) + + // Replace content between markers (inclusive) + newContent := content[:beginIdx] + summary.String() + content[endIdx+len(readmeEndMarker):] + + if err := os.WriteFile("README.md", []byte(newContent), 0644); err != nil { //nolint:gosec // hardcoded path + return fmt.Errorf("writing README.md: %w", err) + } + + return nil +} + +// formatDisplayName converts an interface name like "BGPPeerProvider" to "BGP Peer" +func formatDisplayName(ifaceName string) string { + // Remove "Provider" suffix + name := strings.TrimSuffix(ifaceName, "Provider") + + // Insert spaces before uppercase letters (except at start or consecutive uppercase) + var result strings.Builder + for i, r := range name { + if i > 0 && r >= 'A' && r <= 'Z' { + // Add space if previous char is lowercase, or if next char is lowercase (for acronyms) + if name[i-1] >= 'a' && name[i-1] <= 'z' { + result.WriteByte(' ') + } else if i < len(name)-1 && name[i+1] >= 'a' && name[i+1] <= 'z' { + result.WriteByte(' ') + } + } + result.WriteRune(r) + } + return result.String() +} + +// verifyInterfaceMapComplete checks that interfaceMap contains all *Provider interfaces +// defined in internal/provider/ and there are no stale entries. +func verifyInterfaceMapComplete() error { + sourceInterfaces, err := scanProviderInterfaces("internal/provider") + if err != nil { + return fmt.Errorf("failed to discover provider interfaces: %w", err) + } + + // Check for interfaces in source but missing from interfaceMap + var missing []string + for iface := range sourceInterfaces { + if _, exists := interfaceMap[iface]; !exists { + missing = append(missing, iface) + } + } + + // Check for stale entries in interfaceMap (no longer in source) + var stale []string + for iface := range interfaceMap { + if !sourceInterfaces[iface] { + stale = append(stale, iface) + } + } + + if len(missing) > 0 || len(stale) > 0 { + var errMsg strings.Builder + errMsg.WriteString("interfaceMap is out of sync with source:\n") + + if len(missing) > 0 { + slices.Sort(missing) + fmt.Fprintf(&errMsg, " Missing (defined in source but not in interfaceMap): %v\n", missing) + errMsg.WriteString(" -> Add these to interfaceMap\n") + } + + if len(stale) > 0 { + slices.Sort(stale) + fmt.Fprintf(&errMsg, " Stale (in interfaceMap but not defined in source): %v\n", stale) + errMsg.WriteString(" -> Remove these from interfaceMap\n") + } + + return errors.New(errMsg.String()) + } + + return nil +} + +// scanProviderInterfaces parses Go source files in the provider package and returns +// all interface names matching *Provider (excluding base interfaces like Provider itself). +func scanProviderInterfaces(providerDir string) (map[string]bool, error) { + entries, err := os.ReadDir(providerDir) + if err != nil { + return nil, err + } + + fset := token.NewFileSet() + interfaces := make(map[string]bool) + + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { + continue + } + if strings.HasSuffix(entry.Name(), "_test.go") { + continue + } + + filePath := filepath.Join(providerDir, entry.Name()) + file, err := parser.ParseFile(fset, filePath, nil, 0) + if err != nil { + return nil, fmt.Errorf("parsing %s: %w", filePath, err) + } + + ast.Inspect(file, func(n ast.Node) bool { + typeSpec, ok := n.(*ast.TypeSpec) + if !ok { + return true + } + + _, ok = typeSpec.Type.(*ast.InterfaceType) + if !ok { + return true + } + + name := typeSpec.Name.Name + if !strings.HasSuffix(name, "Provider") { + return true + } + + if baseInterfaces[name] { + return true + } + + interfaces[name] = true + + return true + }) + } + + return interfaces, nil +} + +type providerSpecificType struct { + kind string + coreType string +} + +// extractKind returns the Kind name from a Register*Dependency function name. +// e.g., "RegisterAccessControlListDependency" -> "AccessControlList" +func extractKind(funcName string) string { + return strings.TrimSuffix(strings.TrimPrefix(funcName, dependencyPrefix), dependencySuffix) +} + +// coreTypeInfo holds the mapping between core API types, interface names, and Kubernetes Kind names. +type coreTypeInfo struct { + // e.g., "NetworkVirtualizationEdge" -> "NVEProvider" + coreTypeToInterface map[string]string + // e.g., "NVEProvider" -> "NetworkVirtualizationEdge" + interfaceToKind map[string]string +} + +// buildCoreTypeMapping scans core API type files to build mappings between +// Register*Dependency function names, interfaceMap keys, and Kubernetes Kind names. +// +// The Register*Dependency function names contain the Kubernetes Kind name +// (e.g., RegisterAccessControlListDependency -> Kind="AccessControlList", interface="ACLProvider"). +// The filename convention correlates files to interface names: +// e.g., nve_types.go -> "nve" matches "NVE" in "NVEProvider". +func buildCoreTypeMapping() (*coreTypeInfo, error) { + entries, err := os.ReadDir(coreAPIDir) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", coreAPIDir, err) + } + + fset := token.NewFileSet() + info := &coreTypeInfo{ + coreTypeToInterface: make(map[string]string), + interfaceToKind: make(map[string]string), + } + + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), "_types.go") { + continue + } + if strings.HasSuffix(entry.Name(), "_test.go") { + continue + } + + // Derive interface name from filename: nve_types.go -> nve -> NVEProvider + baseName := strings.TrimSuffix(entry.Name(), "_types.go") + baseName = strings.ReplaceAll(baseName, "_", "") + + var matchedInterface string + for ifaceName := range interfaceMap { + stripped := strings.TrimSuffix(ifaceName, "Provider") + if strings.EqualFold(baseName, stripped) { + matchedInterface = ifaceName + break + } + } + + if matchedInterface == "" { + continue + } + + filePath := filepath.Join(coreAPIDir, entry.Name()) + file, err := parser.ParseFile(fset, filePath, nil, 0) + if err != nil { + return nil, fmt.Errorf("parsing %s: %w", filePath, err) + } + + var inspectErr error + ast.Inspect(file, func(n ast.Node) bool { + funcDecl, ok := n.(*ast.FuncDecl) + if !ok { + return true + } + + name := funcDecl.Name.Name + if strings.HasPrefix(name, dependencyPrefix) && strings.HasSuffix(name, dependencySuffix) { + coreType := extractKind(name) + info.coreTypeToInterface[coreType] = matchedInterface + if existing, ok := info.interfaceToKind[matchedInterface]; ok && existing != coreType { + inspectErr = fmt.Errorf("duplicate mapping for %s: %q and %q in %s", matchedInterface, existing, coreType, entry.Name()) + return false + } + info.interfaceToKind[matchedInterface] = coreType + } + + return true + }) + if inspectErr != nil { + return nil, inspectErr + } + } + + // Verify that each derived Kind name is a real registered type. + // This catches cases where a Register*Dependency function is named + // differently than the actual Kubernetes Kind. + scheme := runtime.NewScheme() + if err := v1alpha1.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("adding core types to scheme: %w", err) + } + for iface, kind := range info.interfaceToKind { + gvk := v1alpha1.GroupVersion.WithKind(kind) + if _, err := scheme.New(gvk); err != nil { + return nil, fmt.Errorf("derived Kind %q for %s is not a registered type: %w", kind, iface, err) + } + } + + return info, nil +} + +// scanConfigExtensions parses Go source files in a provider's API directory and returns +// all config configurations (types that call Register*Dependency in their init function). +func scanConfigExtensions(apiDir string) ([]providerSpecificType, error) { + if apiDir == "" { + return nil, nil + } + + entries, err := os.ReadDir(apiDir) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + return nil, err + } + + fset := token.NewFileSet() + var configurations []providerSpecificType + + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { + continue + } + if strings.HasSuffix(entry.Name(), "_test.go") { + continue + } + + filePath := filepath.Join(apiDir, entry.Name()) + file, err := parser.ParseFile(fset, filePath, nil, 0) + if err != nil { + return nil, fmt.Errorf("parsing %s: %w", filePath, err) + } + + ast.Inspect(file, func(n ast.Node) bool { + funcDecl, ok := n.(*ast.FuncDecl) + if !ok || funcDecl.Name.Name != "init" { + return true + } + + ast.Inspect(funcDecl.Body, func(n ast.Node) bool { + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + funcName := selExpr.Sel.Name + if !strings.HasPrefix(funcName, dependencyPrefix) || !strings.HasSuffix(funcName, dependencySuffix) { + return true + } + + coreType := extractKind(funcName) + + if len(callExpr.Args) > 0 { + if argCall, ok := callExpr.Args[0].(*ast.CallExpr); ok { + if argSel, ok := argCall.Fun.(*ast.SelectorExpr); ok { + if argSel.Sel.Name == "WithKind" && len(argCall.Args) > 0 { + if lit, ok := argCall.Args[0].(*ast.BasicLit); ok { + kind := strings.Trim(lit.Value, `"`) + configurations = append(configurations, providerSpecificType{ + kind: kind, + coreType: coreType, + }) + } + } + } + } + } + + return true + }) + + return true + }) + } + + slices.SortFunc(configurations, func(a, b providerSpecificType) int { + return strings.Compare(a.coreType, b.coreType) + }) + + return configurations, nil +} + +// scanStandaloneTypes parses Go source files in a provider's API directory and returns +// all standalone types (CRDs that are registered but don't extend core types via Register*Dependency). +func scanStandaloneTypes(apiDir string) ([]string, error) { + if apiDir == "" { + return nil, nil + } + + entries, err := os.ReadDir(apiDir) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + return nil, err + } + + fset := token.NewFileSet() + var standaloneKinds []string + + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), "_types.go") { + continue + } + if strings.HasSuffix(entry.Name(), "_test.go") { + continue + } + + filePath := filepath.Join(apiDir, entry.Name()) + file, err := parser.ParseFile(fset, filePath, nil, 0) + if err != nil { + return nil, fmt.Errorf("parsing %s: %w", filePath, err) + } + + hasRegisterDependency := false + ast.Inspect(file, func(n ast.Node) bool { + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + if selExpr, ok := callExpr.Fun.(*ast.SelectorExpr); ok { + if strings.HasPrefix(selExpr.Sel.Name, dependencyPrefix) && strings.HasSuffix(selExpr.Sel.Name, dependencySuffix) { + hasRegisterDependency = true + return false + } + } + return true + }) + + if hasRegisterDependency { + continue + } + + ast.Inspect(file, func(n ast.Node) bool { + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + if ident, ok := selExpr.X.(*ast.Ident); ok { + if ident.Name == "SchemeBuilder" && selExpr.Sel.Name == "Register" { + for _, arg := range callExpr.Args { + if unary, ok := arg.(*ast.UnaryExpr); ok { + if composite, ok := unary.X.(*ast.CompositeLit); ok { + if ident, ok := composite.Type.(*ast.Ident); ok { + if !strings.HasSuffix(ident.Name, "List") { + standaloneKinds = append(standaloneKinds, ident.Name) + } + } + } + } + } + } + } + return true + }) + } + + slices.Sort(standaloneKinds) + return standaloneKinds, nil +} From b580f5b05bf3d83b5d14f1d29f2d0d5cdb022a0e Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Fri, 21 Aug 2026 10:34:35 +0200 Subject: [PATCH 2/2] [docs] Add skill to generate provider compatibility matrix - remove hack script - remove entrypoint from Makefile - add skill for LLM generate compatibility matrix - improve table format to include partial support Co-authored-by: Claude Signed-off-by: Robert Gildein --- .claude/skills/provider-matrix/SKILL.md | 286 ++++++++++ .gitignore | 3 - Makefile | 8 +- Makefile.maker.yaml | 239 -------- README.md | 10 - docs/provider-compatibility.md | 64 ++- hack/generate-provider-matrix.go | 708 ------------------------ 7 files changed, 330 insertions(+), 988 deletions(-) create mode 100644 .claude/skills/provider-matrix/SKILL.md delete mode 100644 Makefile.maker.yaml delete mode 100644 hack/generate-provider-matrix.go diff --git a/.claude/skills/provider-matrix/SKILL.md b/.claude/skills/provider-matrix/SKILL.md new file mode 100644 index 000000000..4b76ac66c --- /dev/null +++ b/.claude/skills/provider-matrix/SKILL.md @@ -0,0 +1,286 @@ +--- +name: provider-matrix +description: Generate or update the provider compatibility matrix at docs/provider-compatibility.md. Reads API types and provider source to determine what each provider supports, what is partially supported (with unsupported fields), and what is platform-exclusive. +allowed-tools: [Read, Bash, Edit, Write] +--- + +# provider-matrix + +Generate the provider compatibility matrix at `docs/provider-compatibility.md`. + +The matrix shows which Kubernetes API types (Kinds) each network device provider supports, +with partial support annotations and platform-exclusive kinds clearly marked. + +> **Important:** Always read the current `docs/provider-compatibility.md` first before making +> any changes. After generating the new content, present it to the user for review. +> **Do not write to the file without explicit user approval.** + +--- + +## Step 1 — Discover all API Kinds + +Scan all `*_types.go` files for types marked `+kubebuilder:object:root=true`. +Exclude `*List` types. Organise by package: + +**Core kinds** — `api/core/v1alpha1/` +These are provider-neutral and map 1:1 to provider interfaces in `internal/provider/provider.go`. +Each core kind has a corresponding `*Provider` interface (e.g. `DNS` → `DNSProvider`). + +**Platform-exclusive kinds** — `api/cisco/nx/v1alpha1/` (and any future `api//*/`) +These are vendor-specific CRDs with no core provider interface. +Scan the directory for kinds and split them into two groups: +- **Config extensions**: have a `Register*Dependency` call in their `init()` — belong in the per-provider "Provider-specific types" table, not the main matrix. +- **Standalone**: no `Register*Dependency` call — appear as rows in the main matrix with N/A for all other providers. + +**Non-provider API packages** — `api/evpn/`, `api/pool/`, `api/cisco/xr/`, `api/cisco/xe/` +These are infrastructure/orchestration types, not provider capability CRDs. +Do **not** include them in the matrix. + +--- + +## Step 2 — Discover provider implementations + +There are three registered providers. For each, check which core `*Provider` interfaces +it implements by looking for compile-time assertions or method presence. + +### Cisco NX-OS +- Source: `internal/provider/cisco/nxos/` +- API: `api/cisco/nx/v1alpha1/` +- **Note:** All interface implementations live in a single large `provider.go` file. To find `NewUnsupportedFieldError` calls for a given interface, identify the method name from the interface definition and search for it in `provider.go`. Helper functions called from that method may also contain violations — read the called functions too. + +### Cisco IOS-XR +- Source: `internal/provider/cisco/iosxr/` +- API: `api/cisco/xr/v1alpha1/` (may be empty — check for any CRDs) +- Read compile-time assertions in `provider.go` to discover implemented interfaces. + +### Nokia SRL (OpenConfig) +- Source: `internal/provider/openconfig/` +- API: none (no OpenConfig-specific CRDs) +- **Note:** Each interface is implemented in its own dedicated file (e.g. `dns.go` → `DNSProvider`). The `var _ provider.XxxProvider` assertion at the top of each file tells you which interface it implements. Scan all files in the directory to collect the full list. + +--- + +## Step 3 — Detect partial support (unsupported fields) + +For each provider+interface combination that is implemented, check whether any +fields are reported as unsupported at runtime. + +### How to find unsupported fields + +Search for `apistatus.NewUnsupportedFieldError` calls in the provider source. +Each call contains one or more `apistatus.FieldViolation{Field: "...", Description: "..."}` literals. +Always read **both** `Field` and `Description`. Use the description to understand what is actually +unsupported — sometimes only specific values of a field are unsupported (e.g. a specific enum value), +not the field itself. In that case, the footnote should mention the specific constraint from the +description, not just the field path. + +**Pattern A — inline:** +```go +return apistatus.NewUnsupportedFieldError(apistatus.FieldViolation{ + Field: "spec.type", + Description: "unsupported interface type: ...", +}) +``` + +**Pattern B — accumulated slice:** +```go +var violations []apistatus.FieldViolation +if condition { + violations = append(violations, apistatus.FieldViolation{ + Field: "spec.adminState", + Description: "adminState Down is not supported", + }) +} +if len(violations) > 0 { + return apistatus.NewUnsupportedFieldError(violations...) +} +``` + +**Pattern C — helper function:** +The `FieldViolation` literals may be in a helper function called from the interface method +(e.g. `validateDNSSpec` called from `EnsureDNS`). Read the called function too. + +**Important:** Only `NewUnsupportedFieldError` indicates a missing feature. +`NewInvalidArgumentError` indicates a bad value for a supported field — do NOT include these. +Common traps that are always `NewInvalidArgumentError`, not unsupported: +- Length or line limits +- Format constraints (e.g. a field must be a specific type or pattern) +- Invalid combinations of supported fields +If the provider's `Ensure*` method reads and uses the field, the field is supported. + +Additionally, do NOT include a field if only some values of it are unsupported — whether that's +a `default:` case for unknown enum values, or a named case for a specific known value. +**Only include a field if the field itself is entirely unsupported** (the provider never reads +or acts on it regardless of its value). + +For `fmt.Sprintf` field values like `fmt.Sprintf("spec.servers[%s].vrfName", addr)`, +normalise the dynamic part to `*`: → `spec.servers[*].vrfName`. + +**Attribution:** To map a `FieldViolation` to an interface, find which interface method +contains (or calls into) the function with the violation. For NX-OS (single `provider.go`), +match by method name (e.g. `EnsureInterface` → `InterfaceProvider`). For OpenConfig, +each file is one interface. + +### How to find ignored fields + +Some fields are silently ignored (not configured on the device) rather than rejected. +These appear as code comments like: +```go +// PrependLocalAS / PrependGlobalAS not supported via OC local-as leaf +// spec.grpc.serverName is not configurable on this platform +``` +Search for `// not supported`, `// unsupported`, `// ignored` comments near field access code. +These should also be listed as partial support notes. + +--- + +## Step 4 — Write the matrix + +Work through Steps 1–3 by reading the source files directly, then write `docs/provider-compatibility.md` +using the output format below. + +**Commands to gather data:** + +```bash +# Core kinds +grep -rn "kubebuilder:object:root=true" api/core/v1alpha1/ | grep -v List + +# NX-OS standalone kinds (no Register*Dependency) +grep -rn "kubebuilder:object:root=true" api/cisco/nx/v1alpha1/ | grep -v List + +# NX-OS config extensions (have Register*Dependency) +grep -rn "Register.*Dependency" api/cisco/nx/v1alpha1/ + +# IOS-XR interface assertions +grep "_ provider\." internal/provider/cisco/iosxr/provider.go + +# OpenConfig interface assertions +grep -rn "var _ provider\." internal/provider/openconfig/ | grep -v _test + +# All NewUnsupportedFieldError field+description values — OpenConfig +grep -A5 "FieldViolation{" internal/provider/openconfig/*.go | grep -E "Field:|Description:" + +# All NewUnsupportedFieldError field+description values — NX-OS +grep -A5 "FieldViolation{" internal/provider/cisco/nxos/provider.go | grep -E "Field:|Description:" +``` + +After collecting the data, write the full markdown directly to `docs/provider-compatibility.md`. +The file header should say ``. + +--- + +## Output format + +### File header + +```markdown + + +# Provider Compatibility Matrix + +This document provides a detailed overview of which API types are supported by each network device provider. + + +``` + +### Compatibility Matrix table + +Columns: `Core Kind` + one column per provider (in registration order). +Rows: all core kinds + platform-exclusive standalone kinds, sorted alphabetically by Kind name. + +```markdown +## Compatibility Matrix + +| Core Kind | Cisco NX-OS | Cisco IOS-XR | Nokia SRL (OpenConfig) | +|-----------|--------|--------|--------| +| `AAA` | ✅ | — | ⚠️ [^1] | +| `AccessControlList` | ✅ | — | ✅ | +| `BorderGateway` | ✅ | N/A | N/A | +| `DNS` | ✅ | — | ⚠️ [^2] | +... +``` + +Cell values: +- `✅` — fully supported (implements the interface, no unsupported fields) +- `⚠️ [^N]` — partially supported (implements the interface but has unsupported fields; footnote lists them) +- `—` — not supported (interface not implemented) +- `N/A` — not applicable (platform-exclusive kind, other providers cannot support it by design) + +Legend (always include): +```markdown +**Legend:** +- ✅ Supported +- ⚠️ Partial support (see footnotes) +- — Not supported +- N/A Not applicable (platform-exclusive feature) +``` + +### Footnotes + +Immediately after the legend, list footnotes in order 1..N (matching their appearance in the table). +Use the description to phrase each entry accurately: +- If the whole field is unsupported: `unsupported fields: \`field\`` + +```markdown +[^1]: **Nokia SRL (OpenConfig) — AAA**: unsupported fields: `spec.authorization`, `spec.serverGroups[].type` +``` + +### Per-provider sections + +One `##` section per provider, in registration order. Each section contains: +- The provider's display name as heading +- A one-line description of the provider +- `**Core API types:** X / Y` where X is the count of implemented core interfaces and Y is the total. Append `+ Z platform-exclusive` if the provider has standalone kinds. +- A `**Provider-specific types:**` table (only if the provider has config extensions or standalone kinds), with columns `Kind` and `Category`. Config extensions show `Extends core type \`\``. Standalone kinds show `Provider-exclusive`. + +Example structure: +```markdown +## + + + +**Core API types:** X / Y (+ Z platform-exclusive if applicable) + +**Provider-specific types:** (omit section if none) + +| Kind | Category | +|------|----------| +| `` | Extends core type `` | +| `` | Provider-exclusive | +``` + +### Contributing section + +```markdown +## Contributing + +To add support for a new API type or provider: + +1. Define the provider interface in `internal/provider/` +2. Implement the interface methods in your provider package +3. Use the `/provider-matrix` skill to regenerate this document +4. Submit a pull request with your changes + +The matrix is automatically generated by checking which provider interfaces +each provider implements, ensuring accuracy and eliminating manual maintenance. +``` + +--- + +## Verification checklist + +After generating, verify: +- [ ] Every `+kubebuilder:object:root=true` non-List kind from `api/core/v1alpha1/` appears as a row (excluding `Device` which is internal) +- [ ] Every standalone kind from each vendor API dir (no `Register*Dependency`) appears as a row with N/A for all providers that don't own it +- [ ] Each provider's core count matches the number of capability interfaces it implements (excluding base interfaces: `Provider`, `ProvisioningProvider`, `DeviceProvider`) +- [ ] Each provider's platform-exclusive count matches the number of standalone kinds in its API dir +- [ ] Footnote numbers are sequential 1..N in left-to-right, top-to-bottom table order +- [ ] Every footnoted field came from `NewUnsupportedFieldError` — not `NewInvalidArgumentError` + +## Final step — Human review + +Present the generated content to the user and wait for approval before writing to `docs/provider-compatibility.md`. +Do not write the file automatically. diff --git a/.gitignore b/.gitignore index 4df9bc69d..90d5ce40a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,9 +21,6 @@ go.work.sum # Generated build-installer dist/* -# Generated provider matrix tool -generate-provider-matrix - # Kubernetes Generated files - skip generated files, except for vendored files !vendor/**/zz_generated.* diff --git a/Makefile b/Makefile index 21c8c9d32..de3244a89 100644 --- a/Makefile +++ b/Makefile @@ -151,7 +151,7 @@ clean: ## Remove all generated files (bin/, dist/, coverage files) ##@ Build .PHONY: docs -docs: crd-ref-docs provider-matrix ## Generate API reference documentation. +docs: crd-ref-docs ## Generate API reference documentation. $(CRD_REF_DOCS) --source-path=./api --config=./hack/api-reference/config.yaml --renderer=markdown --output-path=./docs/api-reference/index.md @sed -i.bak \ -e '/^SPDX-/d' \ @@ -163,12 +163,6 @@ docs: crd-ref-docs provider-matrix ## Generate API reference documentation. docs/api-reference/index.md @find . -type f -name "*.bak" -delete -# Generate provider implementation matrix -.PHONY: provider-matrix -provider-matrix: - @printf "\e[1;36m>> go run ./hack/generate-provider-matrix.go > docs/provider-compatibility.md\e[0m\n" - @go run ./hack/generate-provider-matrix.go > docs/provider-compatibility.md - ROOT_DIR := $(shell pwd) DOCS_IMG ?= ironcore-dev/network-operator-docs:latest diff --git a/Makefile.maker.yaml b/Makefile.maker.yaml deleted file mode 100644 index f32804522..000000000 --- a/Makefile.maker.yaml +++ /dev/null @@ -1,239 +0,0 @@ -# Configuration file for - -metadata: - url: https://github.com/ironcore-dev/network-operator - -binaries: - - name: network-operator - fromPackage: ./cmd - installTo: bin/ - -controllerGen: - enabled: true - crdOutputPath: config/crd/bases - objectHeaderFile: hack/boilerplate.go.txt - applyconfigurationHeaderFile: hack/boilerplate.go.txt - rbacRoleName: manager-role - -coverageTest: - only: "/internal" - -dockerfile: - # Custom Dockerfile using Distroless base image - enabled: false - -golang: - setGoModVersion: true - -golangciLint: - createConfig: true - skipDirs: - - internal/provider/openconfig - timeout: 10m - -goReleaser: - createConfig: true - -license: - addHeaders: true - checkDependencies: true - copyright: 'SAP SE or an SAP affiliate company and IronCore contributors' - spdx: Apache-2.0 - -reuse: - # Custom REUSE.toml with minimal settings - enabled: false - -renovate: - enabled: true - assignees: - - felix-kaestner - -testPackages: - except: '/test' - -githubWorkflow: - global: - defaultBranch: main - ci: - enabled: true - prepareMakeTarget: generate - license: - enabled: true - release: - enabled: true - securityChecks: - enabled: true - pushContainerToGhcr: - enabled: true - platforms: "linux/amd64,linux/arm64" - tagStrategy: - - edge - - latest - - semver - - sha - -variables: - GO_BUILDENV: 'CGO_ENABLED=0' - -verbatim: | - # Image to use all building/pushing image targets - IMG ?= controller:latest - - # CONTAINER_TOOL defines the container tool to be used for building images. - # The default is docker, but it can be overridden to use other tools (i.e. podman or nerdctl). - CONTAINER_TOOL ?= docker - - # KIND_CLUSTER defines the name of the Kind cluster to be used for the tilt setup. - KIND_CLUSTER ?= network - - install-gofumpt: FORCE - @if ! hash gofumpt 2>/dev/null; then printf "\e[1;36m>> Installing gofumpt...\e[0m\n"; go install mvdan.cc/gofumpt@latest; fi - - install-kubebuilder: FORCE - @if ! hash kubebuilder 2>/dev/null; then printf "\e[1;36m>> Installing kubebuilder...\e[0m\n"; go install sigs.k8s.io/kubebuilder/v4@v4.10.1; fi - - install-kustomize: FORCE - @if ! hash kustomize 2>/dev/null; then printf "\e[1;36m>> Installing kustomize...\e[0m\n"; go install sigs.k8s.io/kustomize/kustomize/v5@latest; fi - - install-crd-ref-docs: FORCE - @if ! hash crd-ref-docs 2>/dev/null; then printf "\e[1;36m>> Installing crd-ref-docs...\e[0m\n"; go install github.com/elastic/crd-ref-docs@latest; fi - - fmt: FORCE install-gofumpt - @printf "\e[1;36m>> gofumpt -l -w .\e[0m\n" - @gofumpt -l -w $(shell git ls-files '*.go' | grep -v '^internal/provider/openconfig') - - # Run the e2e tests against a k8s cluster. - test-e2e: FORCE - @command -v kind >/dev/null 2>&1 || { \ - echo "Kind is not installed. Please install Kind manually."; \ - exit 1; \ - } - @kind get clusters | grep -q $(KIND_CLUSTER) || { \ - echo "No Kind cluster is running. Please start a Kind cluster before running the e2e tests."; \ - exit 1; \ - } - @printf "\e[1;36m>> go test ./test/e2e/ -v -ginkgo.v\e[0m\n" - @KIND_CLUSTER=$(KIND_CLUSTER) go test ./test/e2e/ -v -ginkgo.v - - docker-build: FORCE - @printf "\e[1;36m>> $(CONTAINER_TOOL) build --tag=$(IMG) .\e[0m\n" - @$(CONTAINER_TOOL) build --build-arg=BININFO_BUILD_DATE=$(BININFO_BUILD_DATE) --build-arg=BININFO_COMMIT_HASH=$(BININFO_COMMIT_HASH) --build-arg=BININFO_VERSION=$(BININFO_VERSION) --tag=$(IMG) . - - docker-push: FORCE - @printf "\e[1;36m>> $(CONTAINER_TOOL) push $(IMG)\e[0m\n" - @$(CONTAINER_TOOL) push $(IMG) - - # Generate a consolidated YAML with CRDs and deployment. - build-installer: FORCE generate install-kustomize - @printf "\e[1;36m>> kustomize build config/default > dist/install.yaml\e[0m\n" - @mkdir -p dist; kustomize build config/default > dist/install.yaml - - # Deploy controller to the k8s cluster - deploy: FORCE generate install-kustomize - @printf "\e[1;36m>> kustomize build config/default | kubectl apply -f -\e[0m\n" - @kustomize build config/default | kubectl apply -f - - - # Undeploy controller from the k8s cluster - undeploy: FORCE install-kustomize - @printf "\e[1;36m>> kustomize build config/default | kubectl delete -f -\e[0m\n" - @kustomize build config/default | kubectl delete --ignore-not-found=true -f - - - # Install CRDs into the k8s cluster - deploy-crds: FORCE generate install-kustomize - @printf "\e[1;36m>> kustomize build config/crd | kubectl apply -f -\e[0m\n" - @kustomize build config/crd | kubectl apply -f - - - # Uninstall CRDs from the k8s cluster - undeploy-crds: FORCE install-kustomize - @printf "\e[1;36m>> kustomize build config/crd | kubectl delete -f -\e[0m\n" - @kustomize build config/crd | kubectl delete --ignore-not-found=true -f - - - # Create a Kind cluster for local development and testing. - kind-create: FORCE - @command -v kind >/dev/null 2>&1 || { \ - echo "Kind is not installed. Please install Kind manually."; \ - exit 1; \ - } - @kind get clusters | grep -q $(KIND_CLUSTER) || { \ - printf "\e[1;36m>> kind create cluster --name=$(KIND_CLUSTER)\e[0m\n"; \ - kind create cluster --name=$(KIND_CLUSTER); \ - } - - # Delete the Kind cluster created for local development and testing. - kind-delete: FORCE - @command -v kind >/dev/null 2>&1 || { \ - echo "Kind is not installed. Please install Kind manually."; \ - exit 1; \ - } - @printf "\e[1;36m>> kind delete cluster --name=$(KIND_CLUSTER)\e[0m\n" - @kind delete cluster --name=$(KIND_CLUSTER) - - tilt-up: FORCE kind-create - @command -v tilt >/dev/null 2>&1 || { \ - echo "Tilt is not installed. Please install Tilt manually."; \ - exit 1; \ - } - @printf "\e[1;36m>> tilt up --context kind-$(KIND_CLUSTER)\e[0m\n" - @tilt up --context kind-$(KIND_CLUSTER) - - manifests: generate - - # Generate manifests e.g. CRD, RBAC etc. - charts: FORCE install-kubebuilder generate - @printf "\e[1;36m>> kubebuilder edit --plugins=helm/v2-alpha\e[0m\n" - @mv charts/network-operator charts/chart - @kubebuilder edit --plugins=helm/v2-alpha --output-dir=charts - @mv charts/chart charts/network-operator && rm -rf dist - - netop-provider: - @printf "\e[1;36m>> go build -o build/netop-provider ./hack/provider\e[0m\n" - @go build -o build/netop-provider ./hack/provider - @printf "\e[1;36m>> ./build/netop-provider --help\e[0m\n" - @./build/netop-provider --help - - TEST_SERVER_IMG ?= ghcr.io/ironcore-dev/gnmi-test-server:latest - - docker-build-test-gnmi-server: FORCE - @printf "\e[1;36m>> $(CONTAINER_TOOL) build --tag=$(TEST_SERVER_IMG) ./test/gnmi\e[0m\n" - @$(CONTAINER_TOOL) build --tag=$(TEST_SERVER_IMG) ./test/gnmi - - docker-run-test-gnmi-server: FORCE docker-build-test-gnmi-server - @printf "\e[1;36m>> $(CONTAINER_TOOL) run -p 8000:8000 -p 9339:9339 $(TEST_SERVER_IMG)\e[0m\n" - @$(CONTAINER_TOOL) run --rm -p 8000:8000 -p 9339:9339 $(TEST_SERVER_IMG) - - # TEST_LAB_IMG defines the image to used for packaging the lab tests. - TEST_LAB_IMG ?= ghcr.io/ironcore-dev/network-operator-lab-test:latest - - test-lab: FORCE - @printf "\e[1;36m>> go test ./test/lab/ -v\e[0m\n" - @go test ./test/lab/ -v - - docker-build-test-lab: FORCE - @printf "\e[1;36m>> $(CONTAINER_TOOL) build --file=test/lab/Dockerfile --tag=$(TEST_LAB_IMG) .\e[0m\n" - @$(CONTAINER_TOOL) build --file=test/lab/Dockerfile --tag=$(TEST_LAB_IMG) . - - docker-push-test-lab: FORCE docker-push-test-lab - @printf "\e[1;36m>> $(CONTAINER_TOOL) push $(TEST_LAB_IMG)\e[0m\n" - @$(CONTAINER_TOOL) push $(TEST_LAB_IMG) - - ROOT_DIR := $(shell pwd) - DOCS_IMG ?= ironcore-dev/network-operator-docs:latest - - run-docs: - @docker build -t $(DOCS_IMG) -f docs/Dockerfile docs --load - @docker run --rm --init -p 5173:5173 -v $(ROOT_DIR)/docs:/workspace -v /workspace/node_modules $(DOCS_IMG) - - docs: install-crd-ref-docs provider-matrix - crd-ref-docs --source-path=./api --config=./hack/api-reference/config.yaml --renderer=markdown --output-path=./docs/api-reference/index.md - @$(SED) -i \ - -e 's/#networkingmetalironcoredevv1alpha1/#networking-metal-ironcore-dev-v1alpha1/g' \ - -e 's/#nxcisconetworkingmetalironcoredevv1alpha1/#nx-cisco-networking-metal-ironcore-dev-v1alpha1/g' \ - -e 's/#xecisconetworkingmetalironcoredevv1alpha1/#xe-cisco-networking-metal-ironcore-dev-v1alpha1/g' \ - -e 's/#xrcisconetworkingmetalironcoredevv1alpha1/#xr-cisco-networking-metal-ironcore-dev-v1alpha1/g' \ - docs/api-reference/index.md - - # Generate provider implementation matrix - provider-matrix: FORCE - @printf "\e[1;36m>> go run ./hack/generate-provider-matrix.go > docs/provider-compatibility.md\e[0m\n" - @go run ./hack/generate-provider-matrix.go > docs/provider-compatibility.md diff --git a/README.md b/README.md index 96a026c0a..9ad6f1dc1 100644 --- a/README.md +++ b/README.md @@ -105,16 +105,6 @@ kubectl apply -f https://raw.githubusercontent.com//network-operator/ -| Provider | Supported API Types | -|----------|---------------------| -| Cisco NX-OS | 22 / 22 | -| Cisco IOS-XR | 1 / 22 | -| OpenConfig | 1 / 22 | - - For a detailed compatibility matrix showing which API types are supported by each provider, see [Provider Compatibility Matrix](docs/provider-compatibility.md). ## Support, Feedback, Contributing diff --git a/docs/provider-compatibility.md b/docs/provider-compatibility.md index be0a7f261..cd097a16d 100644 --- a/docs/provider-compatibility.md +++ b/docs/provider-compatibility.md @@ -7,49 +7,71 @@ SPDX-License-Identifier: Apache-2.0 This document provides a detailed overview of which API types are supported by each network device provider. - + ## Compatibility Matrix -| Core Kind | Cisco NX-OS | Cisco IOS-XR | OpenConfig | +| Core Kind | Cisco NX-OS | Cisco IOS-XR | Nokia SRL (OpenConfig) | |-----------|--------|--------|--------| -| `AccessControlList` | ✅ | — | — | -| `BGPPeer` | ✅ | — | — | -| `BGP` | ✅ | — | — | -| `Banner` | ✅ | — | — | +| `AAA` | ✅ | — | ⚠️ [^1] | +| `AccessControlList` | ✅ | — | ✅ | +| `BGP` | ✅ | ✅ | ⚠️ [^2] | +| `BGPPeer` | ✅ | ✅ | ⚠️ [^3] | +| `Banner` | ✅ | — | ✅ | +| `BorderGateway` | ✅ | N/A | N/A | | `Certificate` | ✅ | — | — | -| `DNS` | ✅ | — | — | +| `ConfigBackup` | ✅ | — | — | +| `DHCPRelay` | ✅ | — | — | +| `DNS` | ✅ | — | ⚠️ [^4] | | `EVPNInstance` | ✅ | — | — | -| `ISIS` | ✅ | — | — | -| `Interface` | ✅ | ✅ | ✅ | -| `LLDP` | ✅ | — | — | -| `ManagementAccess` | ✅ | — | — | +| `EthernetSegment` | ✅ | — | — | +| `ISIS` | ✅ | — | ✅ | +| `Interface` | ✅ | ✅ | ⚠️ [^5] | +| `LLDP` | ✅ | — | ✅ | +| `Maintenance` | ✅ | ✅ | — | +| `ManagementAccess` | ⚠️ [^6] | — | ⚠️ [^7] | | `NTP` | ✅ | — | — | | `NetworkVirtualizationEdge` | ✅ | — | — | | `OSPF` | ✅ | — | — | | `PIM` | ✅ | — | — | -| `PrefixSet` | ✅ | — | — | -| `RoutingPolicy` | ✅ | — | — | +| `PrefixSet` | ✅ | ✅ | ✅ | +| `RoutingPolicy` | ✅ | — | ⚠️ [^8] | | `SNMP` | ✅ | — | — | -| `Syslog` | ✅ | — | — | +| `Syslog` | ✅ | — | ⚠️ [^9] | +| `System` | ✅ | N/A | N/A | | `User` | ✅ | — | — | | `VLAN` | ✅ | — | — | -| `VRF` | ✅ | — | — | +| `VPCDomain` | ✅ | N/A | N/A | +| `VRF` | ✅ | ✅ | ⚠️ [^10] | **Legend:** - ✅ Supported +- ⚠️ Partial support (see footnotes) - — Not supported +- N/A Not applicable (platform-exclusive feature) + +[^1]: **Nokia SRL (OpenConfig) — AAA**: unsupported fields: `spec.authorization` +[^2]: **Nokia SRL (OpenConfig) — BGP**: unsupported fields: `spec.addressFamilies.l2vpnEvpn` +[^3]: **Nokia SRL (OpenConfig) — BGPPeer**: unsupported fields: `spec.addressFamilies.l2vpnEvpn` +[^4]: **Nokia SRL (OpenConfig) — DNS**: unsupported fields: `spec.adminState`, `spec.servers[*].vrfName`, `spec.sourceInterfaceName` +[^5]: **Nokia SRL (OpenConfig) — Interface**: unsupported fields: `spec.aggregation.multichassis`, `spec.bfd` +[^6]: **Cisco NX-OS — ManagementAccess**: unsupported fields: `spec.grpc.serverName` +[^7]: **Nokia SRL (OpenConfig) — ManagementAccess**: unsupported fields: `spec.grpc.gnmi`, `spec.ssh.sessionLimit` +[^8]: **Nokia SRL (OpenConfig) — RoutingPolicy**: unsupported fields: `spec.statements[].actions.bgpActions.setASPath`, `spec.statements[].actions.bgpActions.setCommunity`, `spec.statements[].actions.bgpActions.setExtCommunity` +[^9]: **Nokia SRL (OpenConfig) — Syslog**: unsupported fields: `spec.facilities`, `spec.servers[].vrfName` +[^10]: **Nokia SRL (OpenConfig) — VRF**: unsupported fields: `spec.routeDistinguisher`, `spec.routeTargets`, `spec.vni` ## Cisco NX-OS Cisco NX-OS provides comprehensive support for network configuration through the network-operator. -**Core API types:** 22 / 22 +**Core API types:** 27 / 27 + 3 platform-exclusive **Provider-specific types:** | Kind | Category | |------|----------| +| `AAAConfig` | Extends core type `AAA` | | `BGPConfig` | Extends core type `BGP` | | `InterfaceConfig` | Extends core type `Interface` | | `LLDPConfig` | Extends core type `LLDP` | @@ -63,13 +85,13 @@ Cisco NX-OS provides comprehensive support for network configuration through the Cisco IOS-XR support is currently in early development. -**Core API types:** 1 / 22 +**Core API types:** 6 / 27 -## OpenConfig +## Nokia SRL (OpenConfig) -OpenConfig provides a vendor-neutral configuration interface using standard OpenConfig YANG models. +OpenConfig provides a vendor-neutral configuration interface using standard OpenConfig YANG models, tested against Nokia SR Linux devices. -**Core API types:** 1 / 22 +**Core API types:** 14 / 27 ## Contributing @@ -77,7 +99,7 @@ To add support for a new API type or provider: 1. Define the provider interface in `internal/provider/` 2. Implement the interface methods in your provider package -3. Run `make provider-matrix` to regenerate this document +3. Use the `/provider-matrix` skill to regenerate this document 4. Submit a pull request with your changes The matrix is automatically generated by checking which provider interfaces diff --git a/hack/generate-provider-matrix.go b/hack/generate-provider-matrix.go deleted file mode 100644 index 163b0c42e..000000000 --- a/hack/generate-provider-matrix.go +++ /dev/null @@ -1,708 +0,0 @@ -// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "errors" - "fmt" - "go/ast" - "go/parser" - "go/token" - "maps" - "os" - "path/filepath" - "reflect" - "slices" - "strings" - - "k8s.io/apimachinery/pkg/runtime" - - v1alpha1 "github.com/ironcore-dev/network-operator/api/core/v1alpha1" - "github.com/ironcore-dev/network-operator/internal/provider" - "github.com/ironcore-dev/network-operator/internal/provider/cisco/iosxr" - "github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos" - "github.com/ironcore-dev/network-operator/internal/provider/openconfig" -) - -// interfaceMap maps provider interface names to their reflect.Type. -// generate-provider-matrix fails if it finds a different set of Provider interfaces in the source code. -var interfaceMap = map[string]reflect.Type{ - "ACLProvider": reflect.TypeFor[provider.ACLProvider](), - "BannerProvider": reflect.TypeFor[provider.BannerProvider](), - "BGPProvider": reflect.TypeFor[provider.BGPProvider](), - "BGPPeerProvider": reflect.TypeFor[provider.BGPPeerProvider](), - "CertificateProvider": reflect.TypeFor[provider.CertificateProvider](), - "DNSProvider": reflect.TypeFor[provider.DNSProvider](), - "EVPNInstanceProvider": reflect.TypeFor[provider.EVPNInstanceProvider](), - "InterfaceProvider": reflect.TypeFor[provider.InterfaceProvider](), - "ISISProvider": reflect.TypeFor[provider.ISISProvider](), - "LLDPProvider": reflect.TypeFor[provider.LLDPProvider](), - "ManagementAccessProvider": reflect.TypeFor[provider.ManagementAccessProvider](), - "NTPProvider": reflect.TypeFor[provider.NTPProvider](), - "NVEProvider": reflect.TypeFor[provider.NVEProvider](), - "OSPFProvider": reflect.TypeFor[provider.OSPFProvider](), - "PIMProvider": reflect.TypeFor[provider.PIMProvider](), - "PrefixSetProvider": reflect.TypeFor[provider.PrefixSetProvider](), - "RoutingPolicyProvider": reflect.TypeFor[provider.RoutingPolicyProvider](), - "SNMPProvider": reflect.TypeFor[provider.SNMPProvider](), - "SyslogProvider": reflect.TypeFor[provider.SyslogProvider](), - "UserProvider": reflect.TypeFor[provider.UserProvider](), - "VLANProvider": reflect.TypeFor[provider.VLANProvider](), - "VRFProvider": reflect.TypeFor[provider.VRFProvider](), -} - -type providerInfo struct { - // name is the short identifier used as map key (e.g., "NXOS") - name string - // displayName is the human-readable name for documentation (e.g., "Cisco NX-OS") - displayName string - // description is shown in the provider section of the generated docs - description string - // apiDir is the path to the provider's API directory (e.g., "./api/cisco/nx/v1alpha1") - // Used to scan for provider-specific config configurations. - apiDir string - // instance is the provider implementation to check for interface support - instance provider.Provider -} - -// providers lists all registered providers to check for interface implementations. -// When adding a new provider, add it here and the matrix will automatically detect -// which interfaces it implements. -var providers = []providerInfo{ - { - name: "NXOS", - displayName: "Cisco NX-OS", - description: "Cisco NX-OS provides comprehensive support for network configuration through the network-operator.", - apiDir: "api/cisco/nx/v1alpha1", - instance: nxos.NewProvider(), - }, - { - name: "IOS-XR", - displayName: "Cisco IOS-XR", - description: "Cisco IOS-XR support is currently in early development.", - apiDir: "api/cisco/xr/v1alpha1", - instance: iosxr.NewProvider(), - }, - { - name: "OpenConfig", - displayName: "OpenConfig", - description: "OpenConfig provides a vendor-neutral configuration interface using standard OpenConfig YANG models.", - apiDir: "", // OpenConfig has no provider-specific API configurations - instance: openconfig.NewProvider(), - }, -} - -const ( - readmeBeginMarker = "" - readmeEndMarker = "" - - coreAPIDir = "api/core/v1alpha1" - dependencyPrefix = "Register" - dependencySuffix = "Dependency" -) - -// baseInterfaces are provider interfaces that don't map to a specific API type -// and should be excluded from the compatibility matrix. -var baseInterfaces = map[string]bool{ - "Provider": true, - "ProvisioningProvider": true, - "DeviceProvider": true, -} - -func main() { - // Verify interfaceMap has all entries by checking source code before generating - if err := verifyInterfaceMapComplete(); err != nil { - fmt.Fprintf(os.Stderr, "Consistency check failed: %v\n", err) - os.Exit(1) - } - - interfaceNames := slices.Sorted(maps.Keys(interfaceMap)) - - // Check implementations for each provider - implementations := make(map[string]map[string]bool) - counts := make(map[string]int) - configurations := make(map[string][]providerSpecificType) - standaloneTypes := make(map[string][]string) - - for _, prov := range providers { - implementations[prov.name] = make(map[string]bool) - providerType := reflect.TypeOf(prov.instance) - - for _, ifaceName := range interfaceNames { - interfaceType := interfaceMap[ifaceName] - implements := providerType.Implements(interfaceType) - implementations[prov.name][ifaceName] = implements - - if implements { - counts[prov.name]++ - } - } - - exts, err := scanConfigExtensions(prov.apiDir) - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: failed to scan config configurations for %s: %v\n", prov.name, err) - } - configurations[prov.name] = exts - - standalone, err := scanStandaloneTypes(prov.apiDir) - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: failed to scan standalone types for %s: %v\n", prov.name, err) - } - standaloneTypes[prov.name] = standalone - } - - // Build mapping from Register*Dependency core types to provider interface names - coreInfo, err := buildCoreTypeMapping() - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: failed to build core type mapping: %v\n", err) - coreInfo = &coreTypeInfo{ - coreTypeToInterface: make(map[string]string), - interfaceToKind: make(map[string]string), - } - } - - // Generate markdown documentation - fmt.Println("") - fmt.Println() - fmt.Println("# Provider Compatibility Matrix") - fmt.Println() - fmt.Println("This document provides a detailed overview of which API types are supported by each network device provider.") - fmt.Println() - fmt.Println("") - fmt.Println() - - fmt.Println("## Compatibility Matrix") - fmt.Println() - - fmt.Print("| Core Kind |") - for _, prov := range providers { - fmt.Printf(" %s |", prov.displayName) - } - fmt.Println() - - fmt.Print("|-----------|") - for range providers { - fmt.Print("--------|") - } - fmt.Println() - - for _, ifaceName := range interfaceNames { - kindName := formatDisplayName(ifaceName) - if k, ok := coreInfo.interfaceToKind[ifaceName]; ok { - kindName = k - } - fmt.Printf("| `%s` |", kindName) - - for _, prov := range providers { - if implementations[prov.name][ifaceName] { - fmt.Print(" ✅ |") - } else { - fmt.Print(" — |") - } - } - fmt.Println() - } - - fmt.Println() - fmt.Println("**Legend:**") - fmt.Println("- ✅ Supported") - fmt.Println("- — Not supported") - fmt.Println() - - // Generate per-provider detail sections - total := len(interfaceMap) - for _, prov := range providers { - fmt.Printf("## %s\n", prov.displayName) - fmt.Println() - fmt.Println(prov.description) - fmt.Println() - fmt.Printf("**Core API types:** %d / %d\n", counts[prov.name], total) - if len(configurations[prov.name]) > 0 || len(standaloneTypes[prov.name]) > 0 { - fmt.Println() - fmt.Println("**Provider-specific types:**") - fmt.Println() - fmt.Println("| Kind | Category |") - fmt.Println("|------|----------|") - for _, ext := range configurations[prov.name] { - displayName := ext.coreType - if ifaceName, ok := coreInfo.coreTypeToInterface[ext.coreType]; ok { - if k, ok := coreInfo.interfaceToKind[ifaceName]; ok { - displayName = k - } else { - displayName = formatDisplayName(ifaceName) - } - } - fmt.Printf("| `%s` | Extends core type `%s` |\n", ext.kind, displayName) - } - for _, kind := range standaloneTypes[prov.name] { - fmt.Printf("| `%s` | Provider-exclusive |\n", kind) - } - } - fmt.Println() - } - - fmt.Println("## Contributing") - fmt.Println() - fmt.Println("To add support for a new API type or provider:") - fmt.Println() - fmt.Println("1. Define the provider interface in `internal/provider/`") - fmt.Println("2. Implement the interface methods in your provider package") - fmt.Println("3. Run `make provider-matrix` to regenerate this document") - fmt.Println("4. Submit a pull request with your changes") - fmt.Println() - fmt.Println("The matrix is automatically generated by checking which provider interfaces") - fmt.Println("each provider implements, ensuring accuracy and eliminating manual maintenance.") - - if err := updateReadmeSummary(counts); err != nil { - fmt.Fprintf(os.Stderr, "Failed to update README.md: %v\n", err) - os.Exit(1) - } -} - -func updateReadmeSummary(counts map[string]int) error { - data, err := os.ReadFile("README.md") - if err != nil { - return fmt.Errorf("reading README.md: %w", err) - } - - content := string(data) - beginIdx := strings.Index(content, readmeBeginMarker) - endIdx := strings.Index(content, readmeEndMarker) - - if beginIdx == -1 || endIdx == -1 { - return fmt.Errorf("markers %q and %q not found in README.md", readmeBeginMarker, readmeEndMarker) - } - - if endIdx <= beginIdx { - return errors.New("end marker appears before begin marker in README.md") - } - - // Build the new summary table - total := len(interfaceMap) - var summary strings.Builder - summary.WriteString(readmeBeginMarker + "\n") - summary.WriteString("| Provider | Supported API Types |\n") - summary.WriteString("|----------|---------------------|\n") - for _, prov := range providers { - fmt.Fprintf(&summary, "| %s | %d / %d |\n", prov.displayName, counts[prov.name], total) - } - summary.WriteString(readmeEndMarker) - - // Replace content between markers (inclusive) - newContent := content[:beginIdx] + summary.String() + content[endIdx+len(readmeEndMarker):] - - if err := os.WriteFile("README.md", []byte(newContent), 0644); err != nil { //nolint:gosec // hardcoded path - return fmt.Errorf("writing README.md: %w", err) - } - - return nil -} - -// formatDisplayName converts an interface name like "BGPPeerProvider" to "BGP Peer" -func formatDisplayName(ifaceName string) string { - // Remove "Provider" suffix - name := strings.TrimSuffix(ifaceName, "Provider") - - // Insert spaces before uppercase letters (except at start or consecutive uppercase) - var result strings.Builder - for i, r := range name { - if i > 0 && r >= 'A' && r <= 'Z' { - // Add space if previous char is lowercase, or if next char is lowercase (for acronyms) - if name[i-1] >= 'a' && name[i-1] <= 'z' { - result.WriteByte(' ') - } else if i < len(name)-1 && name[i+1] >= 'a' && name[i+1] <= 'z' { - result.WriteByte(' ') - } - } - result.WriteRune(r) - } - return result.String() -} - -// verifyInterfaceMapComplete checks that interfaceMap contains all *Provider interfaces -// defined in internal/provider/ and there are no stale entries. -func verifyInterfaceMapComplete() error { - sourceInterfaces, err := scanProviderInterfaces("internal/provider") - if err != nil { - return fmt.Errorf("failed to discover provider interfaces: %w", err) - } - - // Check for interfaces in source but missing from interfaceMap - var missing []string - for iface := range sourceInterfaces { - if _, exists := interfaceMap[iface]; !exists { - missing = append(missing, iface) - } - } - - // Check for stale entries in interfaceMap (no longer in source) - var stale []string - for iface := range interfaceMap { - if !sourceInterfaces[iface] { - stale = append(stale, iface) - } - } - - if len(missing) > 0 || len(stale) > 0 { - var errMsg strings.Builder - errMsg.WriteString("interfaceMap is out of sync with source:\n") - - if len(missing) > 0 { - slices.Sort(missing) - fmt.Fprintf(&errMsg, " Missing (defined in source but not in interfaceMap): %v\n", missing) - errMsg.WriteString(" -> Add these to interfaceMap\n") - } - - if len(stale) > 0 { - slices.Sort(stale) - fmt.Fprintf(&errMsg, " Stale (in interfaceMap but not defined in source): %v\n", stale) - errMsg.WriteString(" -> Remove these from interfaceMap\n") - } - - return errors.New(errMsg.String()) - } - - return nil -} - -// scanProviderInterfaces parses Go source files in the provider package and returns -// all interface names matching *Provider (excluding base interfaces like Provider itself). -func scanProviderInterfaces(providerDir string) (map[string]bool, error) { - entries, err := os.ReadDir(providerDir) - if err != nil { - return nil, err - } - - fset := token.NewFileSet() - interfaces := make(map[string]bool) - - for _, entry := range entries { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { - continue - } - if strings.HasSuffix(entry.Name(), "_test.go") { - continue - } - - filePath := filepath.Join(providerDir, entry.Name()) - file, err := parser.ParseFile(fset, filePath, nil, 0) - if err != nil { - return nil, fmt.Errorf("parsing %s: %w", filePath, err) - } - - ast.Inspect(file, func(n ast.Node) bool { - typeSpec, ok := n.(*ast.TypeSpec) - if !ok { - return true - } - - _, ok = typeSpec.Type.(*ast.InterfaceType) - if !ok { - return true - } - - name := typeSpec.Name.Name - if !strings.HasSuffix(name, "Provider") { - return true - } - - if baseInterfaces[name] { - return true - } - - interfaces[name] = true - - return true - }) - } - - return interfaces, nil -} - -type providerSpecificType struct { - kind string - coreType string -} - -// extractKind returns the Kind name from a Register*Dependency function name. -// e.g., "RegisterAccessControlListDependency" -> "AccessControlList" -func extractKind(funcName string) string { - return strings.TrimSuffix(strings.TrimPrefix(funcName, dependencyPrefix), dependencySuffix) -} - -// coreTypeInfo holds the mapping between core API types, interface names, and Kubernetes Kind names. -type coreTypeInfo struct { - // e.g., "NetworkVirtualizationEdge" -> "NVEProvider" - coreTypeToInterface map[string]string - // e.g., "NVEProvider" -> "NetworkVirtualizationEdge" - interfaceToKind map[string]string -} - -// buildCoreTypeMapping scans core API type files to build mappings between -// Register*Dependency function names, interfaceMap keys, and Kubernetes Kind names. -// -// The Register*Dependency function names contain the Kubernetes Kind name -// (e.g., RegisterAccessControlListDependency -> Kind="AccessControlList", interface="ACLProvider"). -// The filename convention correlates files to interface names: -// e.g., nve_types.go -> "nve" matches "NVE" in "NVEProvider". -func buildCoreTypeMapping() (*coreTypeInfo, error) { - entries, err := os.ReadDir(coreAPIDir) - if err != nil { - return nil, fmt.Errorf("reading %s: %w", coreAPIDir, err) - } - - fset := token.NewFileSet() - info := &coreTypeInfo{ - coreTypeToInterface: make(map[string]string), - interfaceToKind: make(map[string]string), - } - - for _, entry := range entries { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), "_types.go") { - continue - } - if strings.HasSuffix(entry.Name(), "_test.go") { - continue - } - - // Derive interface name from filename: nve_types.go -> nve -> NVEProvider - baseName := strings.TrimSuffix(entry.Name(), "_types.go") - baseName = strings.ReplaceAll(baseName, "_", "") - - var matchedInterface string - for ifaceName := range interfaceMap { - stripped := strings.TrimSuffix(ifaceName, "Provider") - if strings.EqualFold(baseName, stripped) { - matchedInterface = ifaceName - break - } - } - - if matchedInterface == "" { - continue - } - - filePath := filepath.Join(coreAPIDir, entry.Name()) - file, err := parser.ParseFile(fset, filePath, nil, 0) - if err != nil { - return nil, fmt.Errorf("parsing %s: %w", filePath, err) - } - - var inspectErr error - ast.Inspect(file, func(n ast.Node) bool { - funcDecl, ok := n.(*ast.FuncDecl) - if !ok { - return true - } - - name := funcDecl.Name.Name - if strings.HasPrefix(name, dependencyPrefix) && strings.HasSuffix(name, dependencySuffix) { - coreType := extractKind(name) - info.coreTypeToInterface[coreType] = matchedInterface - if existing, ok := info.interfaceToKind[matchedInterface]; ok && existing != coreType { - inspectErr = fmt.Errorf("duplicate mapping for %s: %q and %q in %s", matchedInterface, existing, coreType, entry.Name()) - return false - } - info.interfaceToKind[matchedInterface] = coreType - } - - return true - }) - if inspectErr != nil { - return nil, inspectErr - } - } - - // Verify that each derived Kind name is a real registered type. - // This catches cases where a Register*Dependency function is named - // differently than the actual Kubernetes Kind. - scheme := runtime.NewScheme() - if err := v1alpha1.AddToScheme(scheme); err != nil { - return nil, fmt.Errorf("adding core types to scheme: %w", err) - } - for iface, kind := range info.interfaceToKind { - gvk := v1alpha1.GroupVersion.WithKind(kind) - if _, err := scheme.New(gvk); err != nil { - return nil, fmt.Errorf("derived Kind %q for %s is not a registered type: %w", kind, iface, err) - } - } - - return info, nil -} - -// scanConfigExtensions parses Go source files in a provider's API directory and returns -// all config configurations (types that call Register*Dependency in their init function). -func scanConfigExtensions(apiDir string) ([]providerSpecificType, error) { - if apiDir == "" { - return nil, nil - } - - entries, err := os.ReadDir(apiDir) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil, nil - } - return nil, err - } - - fset := token.NewFileSet() - var configurations []providerSpecificType - - for _, entry := range entries { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { - continue - } - if strings.HasSuffix(entry.Name(), "_test.go") { - continue - } - - filePath := filepath.Join(apiDir, entry.Name()) - file, err := parser.ParseFile(fset, filePath, nil, 0) - if err != nil { - return nil, fmt.Errorf("parsing %s: %w", filePath, err) - } - - ast.Inspect(file, func(n ast.Node) bool { - funcDecl, ok := n.(*ast.FuncDecl) - if !ok || funcDecl.Name.Name != "init" { - return true - } - - ast.Inspect(funcDecl.Body, func(n ast.Node) bool { - callExpr, ok := n.(*ast.CallExpr) - if !ok { - return true - } - - selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) - if !ok { - return true - } - - funcName := selExpr.Sel.Name - if !strings.HasPrefix(funcName, dependencyPrefix) || !strings.HasSuffix(funcName, dependencySuffix) { - return true - } - - coreType := extractKind(funcName) - - if len(callExpr.Args) > 0 { - if argCall, ok := callExpr.Args[0].(*ast.CallExpr); ok { - if argSel, ok := argCall.Fun.(*ast.SelectorExpr); ok { - if argSel.Sel.Name == "WithKind" && len(argCall.Args) > 0 { - if lit, ok := argCall.Args[0].(*ast.BasicLit); ok { - kind := strings.Trim(lit.Value, `"`) - configurations = append(configurations, providerSpecificType{ - kind: kind, - coreType: coreType, - }) - } - } - } - } - } - - return true - }) - - return true - }) - } - - slices.SortFunc(configurations, func(a, b providerSpecificType) int { - return strings.Compare(a.coreType, b.coreType) - }) - - return configurations, nil -} - -// scanStandaloneTypes parses Go source files in a provider's API directory and returns -// all standalone types (CRDs that are registered but don't extend core types via Register*Dependency). -func scanStandaloneTypes(apiDir string) ([]string, error) { - if apiDir == "" { - return nil, nil - } - - entries, err := os.ReadDir(apiDir) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil, nil - } - return nil, err - } - - fset := token.NewFileSet() - var standaloneKinds []string - - for _, entry := range entries { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), "_types.go") { - continue - } - if strings.HasSuffix(entry.Name(), "_test.go") { - continue - } - - filePath := filepath.Join(apiDir, entry.Name()) - file, err := parser.ParseFile(fset, filePath, nil, 0) - if err != nil { - return nil, fmt.Errorf("parsing %s: %w", filePath, err) - } - - hasRegisterDependency := false - ast.Inspect(file, func(n ast.Node) bool { - callExpr, ok := n.(*ast.CallExpr) - if !ok { - return true - } - if selExpr, ok := callExpr.Fun.(*ast.SelectorExpr); ok { - if strings.HasPrefix(selExpr.Sel.Name, dependencyPrefix) && strings.HasSuffix(selExpr.Sel.Name, dependencySuffix) { - hasRegisterDependency = true - return false - } - } - return true - }) - - if hasRegisterDependency { - continue - } - - ast.Inspect(file, func(n ast.Node) bool { - callExpr, ok := n.(*ast.CallExpr) - if !ok { - return true - } - - selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) - if !ok { - return true - } - - if ident, ok := selExpr.X.(*ast.Ident); ok { - if ident.Name == "SchemeBuilder" && selExpr.Sel.Name == "Register" { - for _, arg := range callExpr.Args { - if unary, ok := arg.(*ast.UnaryExpr); ok { - if composite, ok := unary.X.(*ast.CompositeLit); ok { - if ident, ok := composite.Type.(*ast.Ident); ok { - if !strings.HasSuffix(ident.Name, "List") { - standaloneKinds = append(standaloneKinds, ident.Name) - } - } - } - } - } - } - } - return true - }) - } - - slices.Sort(standaloneKinds) - return standaloneKinds, nil -}