mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2026-05-04 14:21:33 +02:00
* refactore(gen): shared functions Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactore(gen): shared functions. simplify metrics Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactore(gen): shared functions. comments Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactore(gen): shared functions. comments Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactore(gen): shared functions. file writes Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactore(gen): shared functions. file writes Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(docs): documentation generators Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(docs): documentation generators Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(docs): documentation generators * refactor(docs): documentation generators Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(docs): documentation generators Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(docs): documentation generators Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * refactor(docs): documentation generators Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> --------- Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
426 lines
12 KiB
Go
426 lines
12 KiB
Go
/*
|
|
Copyright 2025 The Kubernetes 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 main
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
pathToDocs = "%s/../../../../docs/sources"
|
|
fileName = "index.md"
|
|
)
|
|
|
|
func TestIndexMdExists(t *testing.T) {
|
|
testPath, _ := os.Getwd()
|
|
fsys := os.DirFS(fmt.Sprintf(pathToDocs, testPath))
|
|
st, err := fs.Stat(fsys, fileName)
|
|
assert.NoError(t, err, "expected file %s to exist", fileName)
|
|
assert.Equal(t, fileName, st.Name())
|
|
}
|
|
|
|
func TestIndexMdUpToDate(t *testing.T) {
|
|
testPath, _ := os.Getwd()
|
|
fsys := os.DirFS(fmt.Sprintf(pathToDocs, testPath))
|
|
expected, err := fs.ReadFile(fsys, fileName)
|
|
assert.NoError(t, err, "expected file %s to exist", fileName)
|
|
|
|
sourceDir := fmt.Sprintf("%s/../../../../source", testPath)
|
|
sources, err := discoverSources(sourceDir)
|
|
require.NoError(t, err, "expected to find sources")
|
|
actual, err := sources.generateMarkdown()
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, string(expected), actual, "expected file 'docs/sources/index.md' to be up to date. execute 'make generate-sources-documentation'")
|
|
}
|
|
|
|
func TestDiscoverSources(t *testing.T) {
|
|
testPath, _ := os.Getwd()
|
|
sourceDir := fmt.Sprintf("%s/../../../../source", testPath)
|
|
|
|
sources, err := discoverSources(sourceDir)
|
|
require.NoError(t, err)
|
|
|
|
assert.GreaterOrEqual(t, len(sources), 5, "Expected at least 5 sources with annotations")
|
|
|
|
// Verify sources are sorted by name
|
|
for i := range len(sources) - 1 {
|
|
prev, curr := sources[i], sources[i+1]
|
|
if prev.Name > curr.Name {
|
|
t.Errorf("Sources not sorted correctly: %s should come before %s", curr.Name, prev.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGenerateMarkdown(t *testing.T) {
|
|
sources := Sources{
|
|
{
|
|
Name: "test",
|
|
Type: "testSource",
|
|
File: "source/test.go",
|
|
Category: "Test",
|
|
Description: "Test source",
|
|
Resources: "TestResource",
|
|
Filters: "annotation,label",
|
|
Namespace: "all,single",
|
|
FQDNTemplate: "true",
|
|
},
|
|
}
|
|
|
|
content, err := sources.generateMarkdown()
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, content)
|
|
|
|
assert.Contains(t, content, "# Supported Sources")
|
|
assert.Contains(t, content, "## Available Sources")
|
|
}
|
|
|
|
func TestParseSourceAnnotations(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
testFile := filepath.Join(tmpDir, "test_source.go")
|
|
content := `package main
|
|
|
|
// testSource is a test source implementation.
|
|
//
|
|
// +externaldns:source:name=test-source
|
|
// +externaldns:source:category=Testing
|
|
// +externaldns:source:description=A test source for unit testing
|
|
// +externaldns:source:resources=TestResource
|
|
// +externaldns:source:filters=annotation,label
|
|
// +externaldns:source:namespace=all,single
|
|
// +externaldns:source:fqdn-template=true
|
|
// +externaldns:source:provider-specific=true
|
|
type testSource struct {
|
|
client string
|
|
}
|
|
`
|
|
err := os.WriteFile(testFile, []byte(content), 0644)
|
|
require.NoError(t, err)
|
|
|
|
sources, err := parseSourceAnnotations(tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Len(t, sources, 1)
|
|
|
|
source := sources[0]
|
|
assert.Equal(t, "test-source", source.Name)
|
|
assert.Equal(t, "Testing", source.Category)
|
|
assert.Equal(t, "TestResource", source.Resources)
|
|
assert.Equal(t, "annotation,label", source.Filters)
|
|
assert.Equal(t, "all,single", source.Namespace)
|
|
assert.Equal(t, "true", source.FQDNTemplate)
|
|
assert.Equal(t, "false", source.Events)
|
|
assert.Equal(t, "true", source.ProviderSpecific)
|
|
}
|
|
|
|
func TestParseSourceAnnotations_SkipsTestFiles(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a test file that should be skipped
|
|
testFile := filepath.Join(tmpDir, "test_source_test.go")
|
|
content := `package main
|
|
|
|
// +externaldns:source:name=should-be-skipped
|
|
// +externaldns:source:category=Test
|
|
// +externaldns:source:description=Should be skipped
|
|
type testSource struct {}
|
|
`
|
|
err := os.WriteFile(testFile, []byte(content), 0644)
|
|
require.NoError(t, err)
|
|
|
|
sources, err := parseSourceAnnotations(tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, sources)
|
|
}
|
|
|
|
func TestParseFile_MultipleSourcesInOneFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
testFile := filepath.Join(tmpDir, "multi.go")
|
|
content := `package main
|
|
|
|
// firstSource is the first source.
|
|
//
|
|
// +externaldns:source:name=first
|
|
// +externaldns:source:category=Testing
|
|
// +externaldns:source:description=First source
|
|
type firstSource struct {}
|
|
|
|
// secondSource is the second source.
|
|
//
|
|
// +externaldns:source:name=second
|
|
// +externaldns:source:category=Testing
|
|
// +externaldns:source:description=Second source
|
|
// +externaldns:source:events=true
|
|
type secondSource struct {}
|
|
`
|
|
err := os.WriteFile(testFile, []byte(content), 0644)
|
|
require.NoError(t, err)
|
|
|
|
sources, err := parseFile(testFile, tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Len(t, sources, 2)
|
|
assert.Equal(t, "first", sources[0].Name)
|
|
assert.Equal(t, "false", sources[0].Events)
|
|
assert.Equal(t, "false", sources[0].ProviderSpecific)
|
|
assert.Equal(t, "second", sources[1].Name)
|
|
assert.Equal(t, "true", sources[1].Events)
|
|
assert.Equal(t, "false", sources[1].ProviderSpecific)
|
|
}
|
|
|
|
func TestParseFile_IgnoresNonSourceTypes(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
testFile := filepath.Join(tmpDir, "nonsource.go")
|
|
content := `package main
|
|
|
|
// regularStruct is not a source (doesn't end with "Source").
|
|
//
|
|
// +externaldns:source:name=should-not-parse
|
|
// +externaldns:source:category=Test
|
|
// +externaldns:source:description=Should not be parsed
|
|
type regularStruct struct {}
|
|
`
|
|
err := os.WriteFile(testFile, []byte(content), 0644)
|
|
require.NoError(t, err)
|
|
|
|
sources, err := parseFile(testFile, tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, sources)
|
|
}
|
|
|
|
func TestParseSourceAnnotations_ErrorOnInvalidFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a file with invalid Go syntax
|
|
testFile := filepath.Join(tmpDir, "invalid.go")
|
|
content := `package main
|
|
|
|
this is not valid go syntax
|
|
`
|
|
err := os.WriteFile(testFile, []byte(content), 0644)
|
|
require.NoError(t, err)
|
|
|
|
_, err = parseSourceAnnotations(tmpDir)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestParseFile_InvalidGoFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
testFile := filepath.Join(tmpDir, "invalid.go")
|
|
content := `this is not valid go code`
|
|
|
|
err := os.WriteFile(testFile, []byte(content), 0644)
|
|
require.NoError(t, err)
|
|
|
|
_, err = parseFile(testFile, tmpDir)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestParseSourceAnnotations_WithSubdirectories(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
subDir := filepath.Join(tmpDir, "subdir")
|
|
if err := os.Mkdir(subDir, 0755); err != nil {
|
|
t.Fatalf("Failed to create subdirectory: %v", err)
|
|
}
|
|
|
|
// Create a test file in subdirectory
|
|
testFile := filepath.Join(subDir, "nested_source.go")
|
|
content := `package main
|
|
|
|
// nestedSource is in a subdirectory.
|
|
//
|
|
// +externaldns:source:name=nested
|
|
// +externaldns:source:category=Testing
|
|
// +externaldns:source:description=Nested source
|
|
type nestedSource struct {}
|
|
`
|
|
err := os.WriteFile(testFile, []byte(content), 0644)
|
|
require.NoError(t, err)
|
|
|
|
sources, err := parseSourceAnnotations(tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Len(t, sources, 1)
|
|
assert.Equal(t, "nested", sources[0].Name)
|
|
assert.Contains(t, sources[0].File, "subdir/nested_source.go")
|
|
}
|
|
|
|
func TestGenerateMarkdown_WithMultipleCategories(t *testing.T) {
|
|
sources := Sources{
|
|
{
|
|
Name: "service",
|
|
Category: "Kubernetes Core",
|
|
Description: "Service source",
|
|
Resources: "Service",
|
|
Filters: "annotation,label",
|
|
Namespace: "all,single",
|
|
FQDNTemplate: "true",
|
|
},
|
|
{
|
|
Name: "ingress",
|
|
Category: "Kubernetes Core",
|
|
Description: "Ingress source",
|
|
Resources: "Ingress",
|
|
Filters: "annotation,label",
|
|
Namespace: "all,single",
|
|
FQDNTemplate: "true",
|
|
},
|
|
{
|
|
Name: "gateway-httproute",
|
|
Category: "Gateway API",
|
|
Description: "HTTP route source",
|
|
Resources: "HTTPRoute.gateway.networking.k8s.io",
|
|
Filters: "annotation,label",
|
|
Namespace: "all,single",
|
|
FQDNTemplate: "false",
|
|
},
|
|
}
|
|
|
|
content, err := sources.generateMarkdown()
|
|
require.NoError(t, err)
|
|
assert.Contains(t, content, "service")
|
|
assert.Contains(t, content, "ingress")
|
|
assert.Contains(t, content, "gateway-httproute")
|
|
}
|
|
|
|
func TestExtractSourcesFromComments(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
comments string
|
|
typeName string
|
|
filePath string
|
|
wantSources int
|
|
wantErr bool
|
|
validate func(*testing.T, Source)
|
|
}{
|
|
{
|
|
name: "valid single source",
|
|
comments: `testSource is a test implementation.
|
|
|
|
+externaldns:source:name=test
|
|
+externaldns:source:category=Testing
|
|
+externaldns:source:description=A test source
|
|
+externaldns:source:resources=TestResource
|
|
+externaldns:source:filters=annotation
|
|
+externaldns:source:namespace=all
|
|
+externaldns:source:fqdn-template=false
|
|
`,
|
|
typeName: "testSource",
|
|
filePath: "test.go",
|
|
wantSources: 1,
|
|
validate: func(t *testing.T, s Source) {
|
|
assert.Equal(t, "test", s.Name)
|
|
assert.Equal(t, "Testing", s.Category)
|
|
assert.Equal(t, "A test source", s.Description)
|
|
},
|
|
},
|
|
{
|
|
name: "multiple sources in same comment block",
|
|
comments: `gatewaySource handles multiple gateway types.
|
|
|
|
+externaldns:source:name=http-route
|
|
+externaldns:source:category=Gateway
|
|
+externaldns:source:description=Handles HTTP routes
|
|
+externaldns:source:resources=HTTPRoute
|
|
|
|
+externaldns:source:name=tcp-route
|
|
+externaldns:source:category=Gateway
|
|
+externaldns:source:description=Handles TCP routes
|
|
+externaldns:source:resources=TCPRoute
|
|
`,
|
|
typeName: "gatewaySource",
|
|
filePath: "gateway.go",
|
|
wantSources: 2,
|
|
validate: func(t *testing.T, s Source) {
|
|
assert.Contains(t, []string{"http-route", "tcp-route"}, s.Name)
|
|
},
|
|
},
|
|
{
|
|
name: "missing required name annotation",
|
|
comments: `testSource without name.
|
|
|
|
+externaldns:source:category=Testing
|
|
+externaldns:source:description=Missing name
|
|
`,
|
|
typeName: "testSource",
|
|
filePath: "test.go",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "optional annotations can be missing",
|
|
comments: `testSource with minimal annotations.
|
|
|
|
+externaldns:source:name=minimal
|
|
+externaldns:source:category=Testing
|
|
+externaldns:source:description=Minimal source
|
|
`,
|
|
typeName: "testSource",
|
|
filePath: "test.go",
|
|
wantSources: 1,
|
|
validate: func(t *testing.T, s Source) {
|
|
assert.Equal(t, "minimal", s.Name)
|
|
assert.Empty(t, s.Resources)
|
|
assert.Empty(t, s.Filters)
|
|
},
|
|
},
|
|
{
|
|
name: "empty name annotation",
|
|
wantSources: 0,
|
|
comments: `testSource with minimal annotations.
|
|
|
|
+externaldns:source:name=
|
|
+externaldns:source:category=Testing
|
|
+externaldns:source:description=Minimal source
|
|
`,
|
|
typeName: "testSource",
|
|
filePath: "test.go",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
sources, err := extractSourcesFromComments(tt.comments, tt.typeName, tt.filePath)
|
|
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, sources, tt.wantSources)
|
|
|
|
if tt.validate != nil {
|
|
for _, source := range sources {
|
|
tt.validate(t, source)
|
|
}
|
|
}
|
|
|
|
// Verify all sources have required fields
|
|
for _, source := range sources {
|
|
assert.Equal(t, tt.typeName, source.Type)
|
|
assert.Equal(t, tt.filePath, source.File)
|
|
}
|
|
})
|
|
}
|
|
}
|