Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: more e2e tests #183

Merged
merged 2 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
337 changes: 336 additions & 1 deletion pkg/core/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"time"

"github.com/google/uuid"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/avitaltamir/cyphernetes/pkg/core"
"github.com/avitaltamir/cyphernetes/pkg/provider"
"github.com/avitaltamir/cyphernetes/pkg/provider/apiserver"
)

Expand Down Expand Up @@ -1897,7 +1902,6 @@ var _ = Describe("Cyphernetes E2E", func() {
})

It("Should set non-existent annotations", func() {
core.LogLevel = "debug"
By("Creating a deployment without annotations")
testDeployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Expand Down Expand Up @@ -2034,3 +2038,334 @@ var _ = Describe("Cyphernetes E2E", func() {
Expect(k8sClient.Delete(ctx, testDeployment)).Should(Succeed())
})
})

var _ = Describe("Ambiguous Resource Kinds", func() {
It("Should handle ambiguous resource kinds correctly", func() {
ctx := context.Background()

By("Creating a custom resource definition for 'widgets' in a different group")
customWidgetCRD := &apiextensionsv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "widgets.custom.example.com",
},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Group: "custom.example.com",
Names: apiextensionsv1.CustomResourceDefinitionNames{
Plural: "widgets",
Singular: "widget",
Kind: "Widget",
ListKind: "WidgetList",
},
Scope: apiextensionsv1.NamespaceScoped,
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
{
Name: "v1",
Served: true,
Storage: true,
Schema: &apiextensionsv1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensionsv1.JSONSchemaProps{
"spec": {
Type: "object",
Properties: map[string]apiextensionsv1.JSONSchemaProps{
"color": {Type: "string"},
},
},
},
},
},
},
},
},
}

DeferCleanup(func() {
By("Cleaning up the Widget CRD")
err := k8sClient.Delete(ctx, customWidgetCRD)
Expect(err).Should(Or(BeNil(), WithTransform(apierrors.IsNotFound, BeTrue())))

// Wait for the CRD to be fully deleted
Eventually(func() bool {
err := k8sClient.Get(ctx, client.ObjectKey{Name: "widgets.custom.example.com"}, &apiextensionsv1.CustomResourceDefinition{})
return err != nil && apierrors.IsNotFound(err)
}, timeout*2, interval).Should(BeTrue(), "CRD should be deleted")
})

By("Creating the Widget CRD")
Expect(k8sClient.Create(ctx, customWidgetCRD)).Should(Succeed())

// Wait for the CRD to be established
Eventually(func() bool {
var crd apiextensionsv1.CustomResourceDefinition
err := k8sClient.Get(ctx, client.ObjectKey{Name: "widgets.custom.example.com"}, &crd)
if err != nil {
return false
}
for _, cond := range crd.Status.Conditions {
if cond.Type == "Established" && cond.Status == "True" {
return true
}
}
return false
}, timeout, interval).Should(BeTrue(), "First CRD should be established")

By("Creating another Widget CRD in a different group")
otherWidgetCRD := &apiextensionsv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "widgets.other.example.com",
},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Group: "other.example.com",
Names: apiextensionsv1.CustomResourceDefinitionNames{
Plural: "widgets",
Singular: "widget",
Kind: "Widget",
ListKind: "WidgetList",
},
Scope: apiextensionsv1.NamespaceScoped,
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
{
Name: "v1",
Served: true,
Storage: true,
Schema: &apiextensionsv1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensionsv1.JSONSchemaProps{
"spec": {
Type: "object",
Properties: map[string]apiextensionsv1.JSONSchemaProps{
"color": {Type: "string"},
},
},
},
},
},
},
},
},
}

DeferCleanup(func() {
By("Cleaning up the other Widget CRD")
err := k8sClient.Delete(ctx, otherWidgetCRD)
Expect(err).Should(Or(BeNil(), WithTransform(apierrors.IsNotFound, BeTrue())))

// Wait for the CRD to be fully deleted
Eventually(func() bool {
err := k8sClient.Get(ctx, client.ObjectKey{Name: "widgets.other.example.com"}, &apiextensionsv1.CustomResourceDefinition{})
return err != nil && apierrors.IsNotFound(err)
}, timeout*2, interval).Should(BeTrue(), "CRD should be deleted")
})

By("Creating the other Widget CRD")
Expect(k8sClient.Create(ctx, otherWidgetCRD)).Should(Succeed())

// Wait for the CRD to be established
Eventually(func() bool {
var crd apiextensionsv1.CustomResourceDefinition
err := k8sClient.Get(ctx, client.ObjectKey{Name: "widgets.other.example.com"}, &crd)
if err != nil {
return false
}
for _, cond := range crd.Status.Conditions {
if cond.Type == "Established" && cond.Status == "True" {
return true
}
}
return false
}, timeout, interval).Should(BeTrue(), "Second CRD should be established")

By("Creating a provider instance")
provider, err := apiserver.NewAPIServerProvider()
Expect(err).NotTo(HaveOccurred())

By("Verifying ambiguous kind behavior")
// Test case 1: Using just 'Widget' should return error about ambiguity
_, err = provider.FindGVR("Widget")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("ambiguous resource kind"))
Expect(err.Error()).To(ContainSubstring("widgets.custom.example.com"))
Expect(err.Error()).To(ContainSubstring("widgets.other.example.com"))

// Test case 2: Using fully qualified name for first group should work
gvr, err := provider.FindGVR("widgets.custom.example.com")
Expect(err).NotTo(HaveOccurred())
Expect(gvr.Group).To(Equal("custom.example.com"))
Expect(gvr.Resource).To(Equal("widgets"))

// Test case 3: Using fully qualified name for second group should work
gvr, err = provider.FindGVR("widgets.other.example.com")
Expect(err).NotTo(HaveOccurred())
Expect(gvr.Group).To(Equal("other.example.com"))
Expect(gvr.Resource).To(Equal("widgets"))

By("Verifying error handling for invalid inputs")
// Test case 4: Non-existent resource with invalid characters
_, err = provider.FindGVR("ThisIs@CompletelyInvalid!!Resource")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("resource"))
Expect(err.Error()).To(ContainSubstring("not found"))

// Test case 5: Empty input
_, err = provider.FindGVR("")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid resource kind"))

// Test case 6: Malformed group name
_, err = provider.FindGVR("widgets.invalid..group")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not found"))

// Test case 8: Partial group match
_, err = provider.FindGVR("widgets.custom")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not found"))

// Test case 9: Wrong separator
_, err = provider.FindGVR("widgets/custom.example.com")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not found"))
})
})

var _ = Describe("Input Validation", func() {
var provider provider.Provider
var err error

BeforeEach(func() {
provider, err = apiserver.NewAPIServerProvider()
Expect(err).NotTo(HaveOccurred())
})

Context("FindGVR", func() {
It("should handle invalid inputs correctly", func() {
By("Testing empty input")
_, err = provider.FindGVR("")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid resource kind"))

By("Testing input with only dots")
_, err = provider.FindGVR("...")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not found"))

By("Testing input with invalid characters")
_, err = provider.FindGVR("pod$")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not found"))

By("Testing input with only whitespace")
_, err = provider.FindGVR(" ")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not found"))

By("Testing extremely long input")
longInput := strings.Repeat("a", 1000) + ".example.com"
_, err = provider.FindGVR(longInput)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not found"))
})
})

Context("GetK8sResources", func() {
It("should handle invalid inputs correctly", func() {
By("Testing with empty kind")
_, err = provider.GetK8sResources("", "", "", "")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid resource kind"))

By("Testing with invalid field selector")
_, err = provider.GetK8sResources("pod", "invalid==field", "", "default")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("field label not supported"))

By("Testing with invalid label selector")
_, err = provider.GetK8sResources("pod", "", "invalid=label=value", "default")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("found '=', expected: ',' or 'end of string'"))

By("Testing with non-existent namespace")
nonExistentNS := "non-existent-namespace-" + uuid.New().String()
result, err := provider.GetK8sResources("pod", "", "", nonExistentNS)
Expect(err).NotTo(HaveOccurred())
Expect(result).To(BeEmpty(), "Expected empty result for non-existent namespace")
})
})

Context("PatchK8sResource", func() {
It("should handle invalid inputs correctly", func() {
By("Testing with empty kind")
err = provider.PatchK8sResource("", "name", "default", []byte(`[{"op": "add", "path": "/metadata/labels/test", "value": "test"}]`))
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid resource kind"))

By("Creating a test pod")
testPod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-patch-pod",
Namespace: "default",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "test",
Image: "nginx:latest",
},
},
},
}
err = provider.CreateK8sResource("pod", "test-patch-pod", "default", testPod)
Expect(err).NotTo(HaveOccurred())

DeferCleanup(func() {
_ = provider.DeleteK8sResources("pod", "test-patch-pod", "default")
})

By("Testing with invalid JSON patch")
err = provider.PatchK8sResource("pod", "test-patch-pod", "default", []byte(`invalid json`))
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid"))

By("Testing with invalid patch operation")
err = provider.PatchK8sResource("pod", "test-patch-pod", "default", []byte(`[{"op": "invalid", "path": "/metadata/labels/test", "value": "test"}]`))
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("the server rejected our request"))

By("Testing with non-existent resource")
err = provider.PatchK8sResource("pod", "non-existent-pod", "default", []byte(`[{"op": "add", "path": "/metadata/labels/test", "value": "test"}]`))
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not found"))
})
})

Context("Query Execution", func() {
It("should handle invalid query inputs correctly", func() {
executor, err := core.NewQueryExecutor(provider)
Expect(err).NotTo(HaveOccurred())

By("Testing empty query")
_, err = executor.Execute(nil, "default")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("empty query"))

By("Testing malformed MATCH clause")
_, err = core.ParseQuery("MATCH (p:Pod")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("parse error: parsing first clause: expected )"))

By("Testing invalid WHERE clause")
_, err = core.ParseQuery("MATCH (p:Pod) WHERE p.metadata.name = ")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("parse error: parsing first clause: expected value, got \"\""))

By("Testing invalid relationship")
ast, err := core.ParseQuery("MATCH (p:Pod)->(s:NonExistentKind) RETURN p")
Expect(err).NotTo(HaveOccurred())
_, err = executor.Execute(ast, "default")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("error finding API resource >> resource \"NonExistentKind\" not found"))
})
})
})
6 changes: 6 additions & 0 deletions pkg/core/k8s_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,19 @@ func NewQueryExecutor(p provider.Provider) (*QueryExecutor, error) {
}

func (q *QueryExecutor) Execute(ast *Expression, namespace string) (QueryResult, error) {
if ast == nil {
return QueryResult{}, fmt.Errorf("empty query: ast cannot be nil")
}
if len(ast.Contexts) > 0 {
return ExecuteMultiContextQuery(ast, namespace)
}
return q.ExecuteSingleQuery(ast, namespace)
}

func (q *QueryExecutor) ExecuteSingleQuery(ast *Expression, namespace string) (QueryResult, error) {
if ast == nil {
return QueryResult{}, fmt.Errorf("empty query: ast cannot be nil")
}
// Reset match nodes at the start of each query
q.matchNodes = nil

Expand Down
4 changes: 4 additions & 0 deletions pkg/provider/apiserver/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ func (p *APIServerProvider) fetchResources(kind, fieldSelector, labelSelector, n

// Move the FindGVR implementation from k8s_client.go here
func (p *APIServerProvider) FindGVR(kind string) (schema.GroupVersionResource, error) {
if kind == "" {
return schema.GroupVersionResource{}, fmt.Errorf("invalid resource kind: kind cannot be empty")
}

p.gvrCacheMutex.RLock()
defer p.gvrCacheMutex.RUnlock()

Expand Down
Loading