diff --git a/go.mod b/go.mod index e5b88978..833eb79a 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/google/gnostic-models v0.6.9 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.3 + github.com/mattn/go-sqlite3 v1.14.28 github.com/pborman/uuid v1.2.1 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.20.5 diff --git a/go.sum b/go.sum index 7cf4281e..3542751b 100644 --- a/go.sum +++ b/go.sum @@ -182,6 +182,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/pkg/sqlcache/informer/fixtures/README.txt b/pkg/sqlcache/informer/fixtures/README.txt new file mode 100644 index 00000000..df70888c --- /dev/null +++ b/pkg/sqlcache/informer/fixtures/README.txt @@ -0,0 +1,58 @@ +Debugging aids. + +To use this data in sqlite: + +``` +$ cd .../fixtures # (this directory) +$ rm -f test.db +$ sqlite3 test.db + +sqlite> .read schema.txt +sqlite> .read _v1_Namespace.txt +sqlite> .read _v1_Namespace_fields.txt +sqlite> .read _v1_Namespace_labels.txt +sqlite> .read management.cattle.io_v3_Project.txt +sqlite> .read management.cattle.io_v3_Project_fields.txt +sqlite> .read management.cattle.io_v3_Project_labels.txt +sqlite> .read _v1_Foods.txt +sqlite> .read _v1_Foods_fields.txt +sqlite> .read _v1_Foods_labels.txt +``` + +And query the data to your heart's content. + +Some sample queries. Note that you might need to rewrite a query to +have no newlines depending on how you feed it to sqlite. + +#1. + +SELECT o.object, ext1.country, ext1."foodCode" FROM "_v1_Namespace" o + JOIN "_v1_Namespace_fields" f ON o.key = f.key + JOIN "_v1_Foods_fields" ext1 ON f."metadata.fields[2]" = ext1."foodCode" + ORDER BY ext1."country" ASC NULLS LAST + + +#2. + +SELECT __ix_object, __ix_f_metadata_state_name, __ix_ext2_spec_clusterName, __ix_f_metadata_name, __ix_dekid FROM ( + SELECT DISTINCT o.object AS __ix_object, o.objectnonce AS __ix_objectnonce, o.dekid AS __ix_dekid, ext2."spec.clusterName" AS __ix_ext2_spec_clusterName, f."metadata.state.name" AS __ix_f_metadata_state_name, f."metadata.name" AS __ix_f_metadata_name FROM "_v1_Namespace" o + JOIN "_v1_Namespace_fields" f ON o.key = f.key + LEFT OUTER JOIN "_v1_Namespace_labels" lt1 ON o.key = lt1.key + JOIN "management.cattle.io_v3_Project_fields" ext2 ON lt1.value = ext2."metadata.name" + WHERE ((f."metadata.name" LIKE "%cluster-%" ESCAPE '\') OR (f."metadata.name" LIKE "%cattle-%" ESCAPE '\')) AND + (lt1.label = "field.cattle.io/projectId") + +UNION ALL + SELECT DISTINCT o.object AS __ix_object, o.objectnonce AS __ix_objectnonce, o.dekid AS __ix_dekid, NULL AS __ix_ext2_spec_clusterName, NULL AS __ix_f_metadata_state_name, NULL AS __ix_f_metadata_name FROM "_v1_Namespace" o + JOIN "_v1_Namespace_fields" f ON o.key = f.key + LEFT OUTER JOIN "_v1_Namespace_labels" lt1 ON o.key = lt1.key + WHERE ((f."metadata.name" LIKE "%cluster-%" ESCAPE '\') OR (f."metadata.name" LIKE "%cattle-%" ESCAPE '\')) AND + (o.key NOT IN (SELECT o1.key FROM "_v1_Namespace" o1 + JOIN "_v1_Namespace_fields" f1 ON o1.key = f1.key + LEFT OUTER JOIN "_v1_Namespace_labels" lt1i1 ON o1.key = lt1i1.key + WHERE lt1i1.label = "field.cattle.io/projectId")) + +) + ORDER BY __ix_f_metadata_state_name ASC, __ix_ext2_spec_clusterName ASC NULLS LAST, __ix_f_metadata_name ASC + + diff --git a/pkg/sqlcache/informer/fixtures/_v1_Foods.txt b/pkg/sqlcache/informer/fixtures/_v1_Foods.txt new file mode 100644 index 00000000..c9fca2a9 --- /dev/null +++ b/pkg/sqlcache/informer/fixtures/_v1_Foods.txt @@ -0,0 +1,20 @@ +INSERT INTO _v1_Foods VALUES("hot dogs", "hot dogs", "f-nonce-1", 1); +INSERT INTO _v1_Foods VALUES("hamburgers", "hamburgers", "f-nonce-2", 2); +INSERT INTO _v1_Foods VALUES("pears", "pears", "f-nonce-3", 3); +INSERT INTO _v1_Foods VALUES("lemons", "lemons", "f-nonce-4", 4); +INSERT INTO _v1_Foods VALUES("mangoes", "mangoes", "f-nonce-5", 5); +INSERT INTO _v1_Foods VALUES("limes", "limes", "f-nonce-6", 6); +INSERT INTO _v1_Foods VALUES("kiwis", "kiwis", "f-nonce-7", 7); +INSERT INTO _v1_Foods VALUES("plums", "plums", "f-nonce-8", 8); +INSERT INTO _v1_Foods VALUES("bacon", "bacon", "f-nonce-9", 9); +INSERT INTO _v1_Foods VALUES("eggs", "eggs", "f-nonce-10", 10); +INSERT INTO _v1_Foods VALUES("sushi", "sushi", "f-nonce-11", 11); +INSERT INTO _v1_Foods VALUES("tacos", "tacos", "f-nonce-12", 12); +INSERT INTO _v1_Foods VALUES("croissants", "croissants", "f-nonce-13", 13); +INSERT INTO _v1_Foods VALUES("duck", "duck", "f-nonce-14", 14); +INSERT INTO _v1_Foods VALUES("samosas", "samosas", "f-nonce-15", 15); +INSERT INTO _v1_Foods VALUES("kimchi", "kimchi", "f-nonce-16", 16); +INSERT INTO _v1_Foods VALUES("pho", "pho", "f-nonce-17", 17); +INSERT INTO _v1_Foods VALUES("felafel", "felafel", "f-nonce-18", 18); +INSERT INTO _v1_Foods VALUES("ceviche", "ceviche", "f-nonce-19", 19); +INSERT INTO _v1_Foods VALUES("goulash", "goulash", "f-nonce-20", 20); diff --git a/pkg/sqlcache/informer/fixtures/_v1_Foods_fields.txt b/pkg/sqlcache/informer/fixtures/_v1_Foods_fields.txt new file mode 100644 index 00000000..6c5cb55d --- /dev/null +++ b/pkg/sqlcache/informer/fixtures/_v1_Foods_fields.txt @@ -0,0 +1,20 @@ +INSERT INTO _v1_Foods_Fields VALUES("hot dogs", 202, "canada", "active"); +INSERT INTO _v1_Foods_Fields VALUES("hamburgers", 203, "canada", "hungry"); +INSERT INTO _v1_Foods_Fields VALUES("pears", 204, "canada", "muddy"); +INSERT INTO _v1_Foods_Fields VALUES("lemons", 205, "japan", "active"); +INSERT INTO _v1_Foods_Fields VALUES("mangoes", 206, "mexico", "active"); +INSERT INTO _v1_Foods_Fields VALUES("limes", 207, "mexico", "hungry"); +INSERT INTO _v1_Foods_Fields VALUES("kiwis", 208, "new zealand", "active"); +INSERT INTO _v1_Foods_Fields VALUES("plums", 209, "canada", "active"); +INSERT INTO _v1_Foods_Fields VALUES("bacon", 210, "canada", "muddy"); +INSERT INTO _v1_Foods_Fields VALUES("eggs", 211, "canada", "hungry"); +INSERT INTO _v1_Foods_Fields VALUES("sushi", 212, "japan", "active"); +INSERT INTO _v1_Foods_Fields VALUES("tacos", 213, "mexico", "active"); +INSERT INTO _v1_Foods_Fields VALUES("croissants", 214, "france", "active"); +INSERT INTO _v1_Foods_Fields VALUES("duck", 215, "france", "hungry"); +INSERT INTO _v1_Foods_Fields VALUES("karage", 216, "japan", "active"); +INSERT INTO _v1_Foods_Fields VALUES("escargot", 217, "france", "muddy"); +INSERT INTO _v1_Foods_Fields VALUES("pho", 218, "vietnam", "active"); +INSERT INTO _v1_Foods_Fields VALUES("butter tarts", 219, "canada", "hungry"); +INSERT INTO _v1_Foods_Fields VALUES("mochi", 220, "japan", "nervous"); +INSERT INTO _v1_Foods_Fields VALUES("ramen", 221, "japan", "active"); diff --git a/pkg/sqlcache/informer/fixtures/_v1_Foods_labels.txt b/pkg/sqlcache/informer/fixtures/_v1_Foods_labels.txt new file mode 100644 index 00000000..0d9562c7 --- /dev/null +++ b/pkg/sqlcache/informer/fixtures/_v1_Foods_labels.txt @@ -0,0 +1 @@ +SELECT "Nothing to do here"; diff --git a/pkg/sqlcache/informer/fixtures/_v1_Namespace.txt b/pkg/sqlcache/informer/fixtures/_v1_Namespace.txt new file mode 100644 index 00000000..2ecc6804 --- /dev/null +++ b/pkg/sqlcache/informer/fixtures/_v1_Namespace.txt @@ -0,0 +1,22 @@ +INSERT INTO _v1_Namespace VALUES('cluster-01', 'cluster-01', 'ns-nonce2', 2); +INSERT INTO _v1_Namespace VALUES('cluster-02', 'cluster-02', 'ns-nonce2b', 2); +INSERT INTO _v1_Namespace VALUES('cattle-pears', 'cattle-pears', 'ns-nonce3', 3); +INSERT INTO _v1_Namespace VALUES('cattle-lemons', 'cattle-lemons', 'ns-nonce4', 4); +INSERT INTO _v1_Namespace VALUES('cattle-mangoes', 'cattle-mangoes', 'ns-nonce5', 5); +INSERT INTO _v1_Namespace VALUES('cattle-limes', 'cattle-limes', 'ns-nonce6', 6); +INSERT INTO _v1_Namespace VALUES('cattle-kiwis', 'cattle-kiwis', 'ns-nonce9', 9); +INSERT INTO _v1_Namespace VALUES('cattle-plums', 'cattle-plums', 'ns-nonce11', 11); +INSERT INTO _v1_Namespace VALUES('cluster-bacon', 'cluster-bacon', 'ns-nonce13', 13); +INSERT INTO _v1_Namespace VALUES('cluster-eggs', 'cluster-eggs', 'ns-nonce14', 14); +INSERT INTO _v1_Namespace VALUES('default', 'default', 'ns-nonce15', 15); +INSERT INTO _v1_Namespace VALUES('fleet-default', 'fleet-default', 'ns-nonce16', 16); +INSERT INTO _v1_Namespace VALUES('fleet-local', 'fleet-local', 'ns-nonce17', 17); +INSERT INTO _v1_Namespace VALUES('local', 'local', 'ns-nonce21', 21); +INSERT INTO _v1_Namespace VALUES('before-local-01', 'before-local-01', 'ns-nonce21-before01', 21); +INSERT INTO _v1_Namespace VALUES('project-01', 'project-01', 'ns-nonce22', 22); +INSERT INTO _v1_Namespace VALUES('project-02', 'project-02', 'ns-nonce23', 23); +INSERT INTO _v1_Namespace VALUES('project-03', 'project-03', 'ns-nonce24', 24); +INSERT INTO _v1_Namespace VALUES('project-04', 'project-04', 'ns-nonce25', 25); +INSERT INTO _v1_Namespace VALUES('project-05', 'project-05', 'ns-nonce26', 26); +INSERT INTO _v1_Namespace VALUES('user-01', 'user-01', 'ns-nonce27', 27); +INSERT INTO _v1_Namespace VALUES('zebra-01', 'zebra-01', 'ns-nonce28', 28); diff --git a/pkg/sqlcache/informer/fixtures/_v1_Namespace_fields.txt b/pkg/sqlcache/informer/fixtures/_v1_Namespace_fields.txt new file mode 100644 index 00000000..2bfa173a --- /dev/null +++ b/pkg/sqlcache/informer/fixtures/_v1_Namespace_fields.txt @@ -0,0 +1,23 @@ +INSERT INTO _v1_Namespace_fields VALUES('cluster-01','cluster-01','2025-04-11T17:25:28Z','cluster-01','Active','202','cluster-01','active',''); +INSERT INTO _v1_Namespace_fields VALUES('cluster-02','cluster-02','2025-04-11T17:25:28Z','cluster-02','Hungry','203','cluster-02','hungry',''); +INSERT INTO _v1_Namespace_fields VALUES('cattle-pears','cattle-pears','2025-04-11T17:23:36Z','cattle-pears','Muddy','204','cattle-pears','muddy','project-02'); +INSERT INTO _v1_Namespace_fields VALUES('cattle-lemons','cattle-lemons','2025-04-11T17:23:39Z','cattle-lemons','Active','205','cattle-lemons','active','project-02'); +INSERT INTO _v1_Namespace_fields VALUES('cattle-mangoes','cattle-mangoes','2025-04-11T17:22:33Z','cattle-mangoes','Active','206','cattle-mangoes','active','project-02'); +INSERT INTO _v1_Namespace_fields VALUES('cattle-limes','cattle-limes','2025-04-11T17:22:18Z','cattle-limes','Hungry','207','cattle-limes','hungry','project-02'); +INSERT INTO _v1_Namespace_fields VALUES('cattle-kiwis','cattle-kiwis','2025-04-11T17:24:42Z','cattle-kiwis','Active','208','cattle-kiwis','active',''); +INSERT INTO _v1_Namespace_fields VALUES('cattle-plums','cattle-plums','2025-04-11T17:22:02Z','cattle-plums','Active','209','cattle-plums','active',''); +INSERT INTO _v1_Namespace_fields VALUES('cluster-bacon','cluster-bacon','2025-04-11T17:30:39Z','cluster-bacon','Muddy','210','cluster-bacon','muddy','project-02'); +INSERT INTO _v1_Namespace_fields VALUES('cluster-eggs','cluster-eggs','2025-04-11T17:23:36Z','cluster-eggs','Hungry','211','cluster-eggs','hungry','project-02'); +INSERT INTO _v1_Namespace_fields VALUES('default','default','2024-07-08T19:30:47Z','default','Active','212','default','active','project-05'); +INSERT INTO _v1_Namespace_fields VALUES('fleet-default','fleet-default','2025-04-11T17:22:18Z','fleet-default','Active','213','fleet-default','active','project-02'); +INSERT INTO _v1_Namespace_fields VALUES('fleet-local','fleet-local','2025-04-11T17:22:02Z','fleet-local','Active','214','fleet-local','active','project-02'); +INSERT INTO _v1_Namespace_fields VALUES('local','local','2025-04-11T17:22:17Z','local','Hungry','215','local','hungry',''); +INSERT INTO _v1_Namespace_fields VALUES('before-local-01','before-local-01','2025-04-11T17:22:17Z','before-local-01','Active','216','before-local-01','active',''); +INSERT INTO _v1_Namespace_fields VALUES('project-01','project-01','2025-04-11T17:25:28Z','project-01','Muddy','217','project-01','muddy',''); +INSERT INTO _v1_Namespace_fields VALUES('project-02','project-02','2025-04-11T17:22:19Z','project-02','Active','218','project-02','active',''); +INSERT INTO _v1_Namespace_fields VALUES('project-03','project-03','2025-04-11T17:25:28Z','project-03','Hungry','219','project-03','hungry',''); +INSERT INTO _v1_Namespace_fields VALUES('project-04','project-04','2025-04-11T17:36:04Z','project-04','Nervous','220','project-04','nervous',''); +INSERT INTO _v1_Namespace_fields VALUES('project-05','project-05','2025-04-11T17:22:19Z','project-05','Active','221','project-05','active',''); +INSERT INTO _v1_Namespace_fields VALUES('project-06','project-06','2025-04-11T17:22:19Z','project-06','Muddy','222','project-06','muddy',''); +INSERT INTO _v1_Namespace_fields VALUES('user-01','user-01','2025-04-11T17:23:27Z','user-01','Active','223','user-01','active',''); +INSERT INTO _v1_Namespace_fields VALUES('zebra-01','zebra-01','2025-04-11T17:23:27Z','zebra-01','Active','224','zebra-01','active',''); diff --git a/pkg/sqlcache/informer/fixtures/_v1_Namespace_labels.txt b/pkg/sqlcache/informer/fixtures/_v1_Namespace_labels.txt new file mode 100644 index 00000000..39c4b359 --- /dev/null +++ b/pkg/sqlcache/informer/fixtures/_v1_Namespace_labels.txt @@ -0,0 +1,36 @@ +INSERT INTO _v1_Namespace_labels VALUES('cluster-01','kubernetes.io/metadata.name','cluster-01'); +INSERT INTO _v1_Namespace_labels VALUES('cattle-pears','kubernetes.io/metadata.name','cattle-pears'); +INSERT INTO _v1_Namespace_labels VALUES('cattle-pears','objectset.rio.cattle.io/hash','4510303f19b6cbafc9031148b1866f16c2be7aaa'); +INSERT INTO _v1_Namespace_labels VALUES('cattle-pears','field.cattle.io/projectId','project-02'); +INSERT INTO _v1_Namespace_labels VALUES('cattle-lemons','field.cattle.io/projectId','project-01'); +INSERT INTO _v1_Namespace_labels VALUES('cattle-lemons','kubernetes.io/metadata.name','cattle-lemons'); +INSERT INTO _v1_Namespace_labels VALUES('cattle-lemons','objectset.rio.cattle.io/hash','362023f752e7f1989d8b652e029bd2c658ae7c44'); +INSERT INTO _v1_Namespace_labels VALUES('cattle-mangoes','field.cattle.io/projectId','project-02'); +INSERT INTO _v1_Namespace_labels VALUES('cattle-mangoes','kubernetes.io/metadata.name','cattle-mangoes'); +INSERT INTO _v1_Namespace_labels VALUES('cattle-mangoes','objectset.rio.cattle.io/hash','4510303f19b6cbafc9031148b1866f16c2be7aaa'); +INSERT INTO _v1_Namespace_labels VALUES('cattle-limes','cattle.io/creator','norman'); +INSERT INTO _v1_Namespace_labels VALUES('cattle-limes','field.cattle.io/projectId','project-03'); +INSERT INTO _v1_Namespace_labels VALUES('cattle-limes','kubernetes.io/metadata.name','cattle-limes'); +INSERT INTO _v1_Namespace_labels VALUES('cattle-kiwis','kubernetes.io/metadata.name','cattle-kiwis'); +INSERT INTO _v1_Namespace_labels VALUES('cattle-plums','kubernetes.io/metadata.name','cattle-plums'); +INSERT INTO _v1_Namespace_labels VALUES('cluster-bacon','field.cattle.io/projectId','project-02'); +INSERT INTO _v1_Namespace_labels VALUES('cluster-bacon','fleet.cattle.io/managed','true'); +INSERT INTO _v1_Namespace_labels VALUES('cluster-bacon','kubernetes.io/metadata.name','cluster-bacon'); +INSERT INTO _v1_Namespace_labels VALUES('cluster-eggs','field.cattle.io/projectId','project-04'); +INSERT INTO _v1_Namespace_labels VALUES('cluster-eggs','fleet.cattle.io/managed','true'); +INSERT INTO _v1_Namespace_labels VALUES('cluster-eggs','kubernetes.io/metadata.name','cluster-eggs'); +INSERT INTO _v1_Namespace_labels VALUES('default','field.cattle.io/projectId','project-05'); +INSERT INTO _v1_Namespace_labels VALUES('default','kubernetes.io/metadata.name','default'); +INSERT INTO _v1_Namespace_labels VALUES('fleet-default','field.cattle.io/projectId','project-04'); +INSERT INTO _v1_Namespace_labels VALUES('fleet-default','kubernetes.io/metadata.name','fleet-default'); +INSERT INTO _v1_Namespace_labels VALUES('fleet-default','objectset.rio.cattle.io/hash','7e7ea272a8ed59a1ca6ea1587cb55d693b393102'); +INSERT INTO _v1_Namespace_labels VALUES('fleet-local','field.cattle.io/projectId','project-03'); +INSERT INTO _v1_Namespace_labels VALUES('fleet-local','kubernetes.io/metadata.name','fleet-local'); +INSERT INTO _v1_Namespace_labels VALUES('local','kubernetes.io/metadata.name','local'); +INSERT INTO _v1_Namespace_labels VALUES('project-01','kubernetes.io/metadata.name','project-01'); +INSERT INTO _v1_Namespace_labels VALUES('project-02','kubernetes.io/metadata.name','project-02'); +INSERT INTO _v1_Namespace_labels VALUES('project-03','kubernetes.io/metadata.name','project-03'); +INSERT INTO _v1_Namespace_labels VALUES('project-04','kubernetes.io/metadata.name','project-04'); +INSERT INTO _v1_Namespace_labels VALUES('project-05','kubernetes.io/metadata.name','project-05'); +INSERT INTO _v1_Namespace_labels VALUES('project-06','kubernetes.io/metadata.name','project-06'); +INSERT INTO _v1_Namespace_labels VALUES('user-01','kubernetes.io/metadata.name','user-01'); diff --git a/pkg/sqlcache/informer/fixtures/management.cattle.io_v3_Project.txt b/pkg/sqlcache/informer/fixtures/management.cattle.io_v3_Project.txt new file mode 100644 index 00000000..666eb2dc --- /dev/null +++ b/pkg/sqlcache/informer/fixtures/management.cattle.io_v3_Project.txt @@ -0,0 +1,6 @@ +INSERT INTO "management.cattle.io_v3_Project" VALUES("cluster-01/project-01", 'mcioproj-obj1', 'mcioproj-nonce1', 1); +INSERT INTO "management.cattle.io_v3_Project" VALUES("cluster-01/project-03", 'mcioproj-obj2', 'mcioproj-nonce2', 2); +INSERT INTO "management.cattle.io_v3_Project" VALUES("cluster-01/project-04", 'mcioproj-obj3', 'mcioproj-nonce3', 3); +INSERT INTO "management.cattle.io_v3_Project" VALUES("local/project-02", 'mcioproj-obj4', 'mcioproj-nonce4', 4); +INSERT INTO "management.cattle.io_v3_Project" VALUES("local/project-05", 'mcioproj-obj5', 'mcioproj-nonce5', 5); +INSERT INTO "management.cattle.io_v3_Project" VALUES("local/project-06", 'mcioproj-obj6', 'mcioproj-nonce6', 6); diff --git a/pkg/sqlcache/informer/fixtures/management.cattle.io_v3_Project_fields.txt b/pkg/sqlcache/informer/fixtures/management.cattle.io_v3_Project_fields.txt new file mode 100644 index 00000000..34ccdbe6 --- /dev/null +++ b/pkg/sqlcache/informer/fixtures/management.cattle.io_v3_Project_fields.txt @@ -0,0 +1,6 @@ +INSERT INTO "management.cattle.io_v3_Project_fields" VALUES('cluster-01/project-01','project-01','2025-04-11T17:25:28Z','cluster-01','project-01','4h1m','cluster-01/project-01','active','cluster-01'); +INSERT INTO "management.cattle.io_v3_Project_fields" VALUES('cluster-02/project-03','project-03','2025-04-11T17:25:28Z','cluster-02','project-03','4h1m','cluster-02/project-03','active','cluster-02'); +INSERT INTO "management.cattle.io_v3_Project_fields" VALUES('cluster-01/project-04','project-04','2025-04-11T17:36:04Z','cluster-01','project-04','3h50m','cluster-01/project-04','active','cluster-01'); +INSERT INTO "management.cattle.io_v3_Project_fields" VALUES('local/project-02','project-02','2025-04-11T17:22:19Z','local','project-02','4h4m','local/project-02','active','local'); +INSERT INTO "management.cattle.io_v3_Project_fields" VALUES('local/project-05','project-05','2025-04-11T17:22:19Z','local','project-05','4h4m','local/project-05','active','local'); +INSERT INTO "management.cattle.io_v3_Project_fields" VALUES('local/project-06','project-06','2025-04-11T17:22:19Z','local','project-06','4h4m','local/project-06','active','before-local-01'); diff --git a/pkg/sqlcache/informer/fixtures/management.cattle.io_v3_Project_labels.txt b/pkg/sqlcache/informer/fixtures/management.cattle.io_v3_Project_labels.txt new file mode 100644 index 00000000..d1cba60f --- /dev/null +++ b/pkg/sqlcache/informer/fixtures/management.cattle.io_v3_Project_labels.txt @@ -0,0 +1,5 @@ +INSERT INTO "management.cattle.io_v3_Project_labels" VALUES('cluster-01/project-01','authz.management.cattle.io/default-project','true'); +INSERT INTO "management.cattle.io_v3_Project_labels" VALUES('cluster-01/project-03','authz.management.cattle.io/system-project','true'); +INSERT INTO "management.cattle.io_v3_Project_labels" VALUES('cluster-01/project-04','cattle.io/creator','norman'); +INSERT INTO "management.cattle.io_v3_Project_labels" VALUES('local/project-02','authz.management.cattle.io/system-project','true'); +INSERT INTO "management.cattle.io_v3_Project_labels" VALUES('local/project-05','authz.management.cattle.io/default-project','true'); diff --git a/pkg/sqlcache/informer/fixtures/schema.txt b/pkg/sqlcache/informer/fixtures/schema.txt new file mode 100644 index 00000000..4222c511 --- /dev/null +++ b/pkg/sqlcache/informer/fixtures/schema.txt @@ -0,0 +1,99 @@ +/* Tables and indices for _v1_Namespace and without the blobs */ + +CREATE TABLE IF NOT EXISTS "_v1_Namespace" ( + key TEXT UNIQUE NOT NULL PRIMARY KEY, + object TEXT, + objectnonce TEXT, + dekid INTEGER + ); +CREATE TABLE IF NOT EXISTS "_v1_Namespace_indices" ( + name TEXT NOT NULL, + value TEXT NOT NULL, + key TEXT NOT NULL REFERENCES "_v1_Namespace"(key) ON DELETE CASCADE, + PRIMARY KEY (name, value, key) + ); +CREATE INDEX "_v1_Namespace_indices_index" ON "_v1_Namespace_indices"(name, value); +CREATE TABLE IF NOT EXISTS "_v1_Namespace_fields" ( + key TEXT NOT NULL PRIMARY KEY, + "metadata.name" TEXT, "metadata.creationTimestamp" TEXT, "metadata.fields[0]" TEXT, "metadata.fields[1]" TEXT, "metadata.fields[2]" TEXT, "id" TEXT, "metadata.state.name" TEXT, "metadata.labels[field.cattle.io/projectId]" TEXT + ); +CREATE INDEX "_v1_Namespace_metadata.name_index" ON "_v1_Namespace_fields"("metadata.name"); +CREATE INDEX "_v1_Namespace_metadata.creationTimestamp_index" ON "_v1_Namespace_fields"("metadata.creationTimestamp"); +CREATE INDEX "_v1_Namespace_metadata.fields[0]_index" ON "_v1_Namespace_fields"("metadata.fields[0]"); +CREATE INDEX "_v1_Namespace_metadata.fields[1]_index" ON "_v1_Namespace_fields"("metadata.fields[1]"); +CREATE INDEX "_v1_Namespace_metadata.fields[2]_index" ON "_v1_Namespace_fields"("metadata.fields[2]"); +CREATE INDEX "_v1_Namespace_id_index" ON "_v1_Namespace_fields"("id"); +CREATE INDEX "_v1_Namespace_metadata.state.name_index" ON "_v1_Namespace_fields"("metadata.state.name"); +CREATE INDEX "_v1_Namespace_metadata.labels[field.cattle.io/projectId]_index" ON "_v1_Namespace_fields"("metadata.labels[field.cattle.io/projectId]"); +CREATE TABLE IF NOT EXISTS "_v1_Namespace_labels" ( + key TEXT NOT NULL REFERENCES "_v1_Namespace"(key) ON DELETE CASCADE, + label TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY (key, label) + ); +CREATE INDEX "_v1_Namespace_labels_index" ON "_v1_Namespace_labels"(label, value); +CREATE TABLE IF NOT EXISTS "management.cattle.io_v3_Project" ( + key TEXT UNIQUE NOT NULL PRIMARY KEY, + object TEXT, + objectnonce TEXT, + dekid INTEGER + ); + +CREATE TABLE IF NOT EXISTS "management.cattle.io_v3_Project_indices" ( + name TEXT NOT NULL, + value TEXT NOT NULL, + key TEXT NOT NULL REFERENCES "management.cattle.io_v3_Project"(key) ON DELETE CASCADE, + PRIMARY KEY (name, value, key) + ); +CREATE INDEX "management.cattle.io_v3_Project_indices_index" ON "management.cattle.io_v3_Project_indices"(name, value); +CREATE TABLE IF NOT EXISTS "management.cattle.io_v3_Project_fields" ( + key TEXT NOT NULL PRIMARY KEY, + "metadata.name" TEXT, "metadata.creationTimestamp" TEXT, "metadata.namespace" TEXT, "metadata.fields[0]" TEXT, "metadata.fields[1]" TEXT, "id" TEXT, "metadata.state.name" TEXT, "spec.clusterName" TEXT + ); +CREATE INDEX "management.cattle.io_v3_Project_metadata.name_index" ON "management.cattle.io_v3_Project_fields"("metadata.name"); +CREATE INDEX "management.cattle.io_v3_Project_metadata.creationTimestamp_index" ON "management.cattle.io_v3_Project_fields"("metadata.creationTimestamp"); +CREATE INDEX "management.cattle.io_v3_Project_metadata.namespace_index" ON "management.cattle.io_v3_Project_fields"("metadata.namespace"); +CREATE INDEX "management.cattle.io_v3_Project_metadata.fields[0]_index" ON "management.cattle.io_v3_Project_fields"("metadata.fields[0]"); +CREATE INDEX "management.cattle.io_v3_Project_metadata.fields[1]_index" ON "management.cattle.io_v3_Project_fields"("metadata.fields[1]"); +CREATE INDEX "management.cattle.io_v3_Project_id_index" ON "management.cattle.io_v3_Project_fields"("id"); +CREATE INDEX "management.cattle.io_v3_Project_metadata.state.name_index" ON "management.cattle.io_v3_Project_fields"("metadata.state.name"); +CREATE INDEX "management.cattle.io_v3_Project_spec.clusterName_index" ON "management.cattle.io_v3_Project_fields"("spec.clusterName"); +CREATE TABLE IF NOT EXISTS "management.cattle.io_v3_Project_labels" ( + key TEXT NOT NULL REFERENCES "management.cattle.io_v3_Project"(key) ON DELETE CASCADE, + label TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY (key, label) + ); +CREATE INDEX "management.cattle.io_v3_Project_labels_index" ON "management.cattle.io_v3_Project_labels"(label, value); + +CREATE TABLE IF NOT EXISTS "_v1_Foods" ( + key TEXT UNIQUE NOT NULL PRIMARY KEY, + object TEXT, + objectnonce TEXT, + dekid INTEGER + ); +CREATE TABLE IF NOT EXISTS "_v1_Foods_indices" ( + name TEXT NOT NULL, + value TEXT NOT NULL, + key TEXT NOT NULL REFERENCES "_v1_Foods"(key) ON DELETE CASCADE, + PRIMARY KEY (name, value, key) + ); +CREATE INDEX "_v1_Foods_indices_index" ON "_v1_Foods_indices"(name, value); + +CREATE TABLE IF NOT EXISTS "_v1_Foods_fields" ( + key TEXT NOT NULL PRIMARY KEY, + foodCode INTEGER, + country TEXT, + state TEXT); +CREATE INDEX "_v1_Foods_key_index" ON "_v1_Foods_fields"("key"); +CREATE INDEX "_v1_Foods_foodCode_index" ON "_v1_Foods_fields"("foodCode"); +CREATE INDEX "_v1_Foods_country_index" ON "_v1_Foods_fields"("country"); +CREATE INDEX "_v1_Foods_state_index" ON "_v1_Foods_fields"("state"); +CREATE TABLE IF NOT EXISTS "_v1_Foods_labels" ( + key TEXT NOT NULL REFERENCES "_v1_Foods"(key) ON DELETE CASCADE, + label TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY (key, label) + ); +CREATE INDEX "_v1_Foods_labels_index" ON "_v1_Foods_labels"(label, value); + diff --git a/pkg/sqlcache/informer/verify_generator_test.go b/pkg/sqlcache/informer/verify_generator_test.go new file mode 100644 index 00000000..95fa4b50 --- /dev/null +++ b/pkg/sqlcache/informer/verify_generator_test.go @@ -0,0 +1,667 @@ +/* +Copyright 2024, 2025 SUSE LLC +*/ + +package informer + +import ( + "context" + "database/sql" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + _ "github.com/mattn/go-sqlite3" + "github.com/rancher/apiserver/pkg/types" + "github.com/rancher/steve/pkg/sqlcache/partition" + "github.com/rancher/steve/pkg/sqlcache/sqltypes" + "github.com/rancher/steve/pkg/stores/sqlpartition/listprocessor" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/tools/cache" +) + +var vaiFile *os.File +var dbClient *sql.DB + +func getListOptionIndexer(t *testing.T, ctx context.Context, namespaced bool) (*ListOptionIndexer, error) { + myStore := NewMockStore(gomock.NewController(t)) + + i := &Indexer{ + ctx: ctx, + Store: myStore, + indexers: cache.Indexers{}, + } + fields := []string{"metadata.name", "metadata.creationTimestamp", "metadata.fields[0]", "metadata.fields[1]", "metadata.fields[2]", "id", "metadata.state.name"} + l := &ListOptionIndexer{ + Indexer: i, + namespaced: namespaced, + indexedFields: fields, + } + return l, nil +} + +func getListOptionIndexerForQuery(t *testing.T, ctx context.Context, query string) (*ListOptionIndexer, *sqltypes.ListOptions, error) { + l, err := getListOptionIndexer(t, ctx, false) + if err != nil { + return nil, nil, err + } + apiOp := &types.APIRequest{ + Type: "namespace", + Method: "GET", + Namespace: "", + Request: &http.Request{ + Method: "GET", + URL: &url.URL{ + RawQuery: query, + }, + }, + } + lo, err := listprocessor.ParseQuery(apiOp, nil) + return l, &lo, err +} + +func TestEmptyFilter(t *testing.T) { + l, lo, err := getListOptionIndexerForQuery(t, context.Background(), "") + require.Nil(t, err) + p := partition.Partition{Passthrough: true} + partitions := []partition.Partition{p} + namespace := "" + queryInfo, err := l.constructQuery(lo, partitions, namespace, "_v1_Namespace") + require.Nil(t, err) + require.Equal(t, queryInfo.query, "SELECT o.object, o.objectnonce, o.dekid FROM \"_v1_Namespace\" o\n JOIN \"_v1_Namespace_fields\" f ON o.key = f.key\n ORDER BY f.\"metadata.name\" ASC\n LIMIT ?") + require.Equal(t, 1, len(queryInfo.params)) + q := queryInfo.query + q = "SELECT o.key, " + q[len("SELECT")+1:] + stmt, err := dbClient.Prepare(q) + require.Nil(t, err) + defer stmt.Close() + rows, err := stmt.Query(queryInfo.params[0]) + require.Nil(t, err) + numRows := 0 + for rows.Next() { + numRows += 1 + var key string + var o2, o3, o4 string + err = rows.Scan(&key, &o2, &o3, &o4) + require.Nil(t, err) + require.NotEqual(t, "", key) + } + err = rows.Err() + require.Nil(t, err) + assert.Equal(t, 22, numRows) +} + +func TestSimpleFilterOnName(t *testing.T) { + l, lo, err := getListOptionIndexerForQuery(t, context.Background(), "filter=metadata.name~cluster-") + require.Nil(t, err) + p := partition.Partition{Passthrough: true} + partitions := []partition.Partition{p} + namespace := "" + queryInfo, err := l.constructQuery(lo, partitions, namespace, "_v1_Namespace") + require.Nil(t, err) + // Other tests should verify the returned query, so these tests are just for sanity checking + require.True(t, strings.Contains(queryInfo.query, "f.\"metadata.name\" LIKE")) + require.True(t, strings.Contains(queryInfo.query, "ORDER BY f.\"metadata.name\" ASC")) + require.Equal(t, len(queryInfo.params), 2) + require.Equal(t, queryInfo.params[0], "%cluster-%") + q := queryInfo.query + q = "SELECT o.key, " + q[len("SELECT")+1:] + stmt, err := dbClient.Prepare(q) + require.Nil(t, err) + defer stmt.Close() + rows, err := stmt.Query(queryInfo.params...) + require.Nil(t, err) + numRows := 0 + sawKeys := sets.NewString() + for rows.Next() { + numRows += 1 + var key string + var o2, o3, o4 string + err = rows.Scan(&key, &o2, &o3, &o4) + require.Nil(t, err) + require.NotEqual(t, "", key) + require.False(t, sawKeys.Has(key)) + sawKeys.Insert(key) + } + err = rows.Err() + require.Nil(t, err) + assert.Equal(t, 4, numRows) + assert.True(t, sawKeys.HasAll("cluster-01", "cluster-02", "cluster-bacon", "cluster-eggs")) +} + +func wrapStartOfQueryGetRows(t *testing.T, ctx context.Context, query string) (*sql.Rows, error) { + l, lo, err := getListOptionIndexerForQuery(t, ctx, query) + if err != nil { + return nil, err + } + p := partition.Partition{Passthrough: true} + partitions := []partition.Partition{p} + namespace := "" + queryInfo, err := l.constructQuery(lo, partitions, namespace, "_v1_Namespace") + if err != nil { + return nil, err + } + stmt, err := dbClient.Prepare(queryInfo.query) + if err != nil { + return nil, err + } + defer stmt.Close() + return stmt.Query(queryInfo.params...) +} + +func getFirstFieldFromRows(rows *sql.Rows) ([]string, error) { + names := make([]string, 0) + for rows.Next() { + var key string + var o2, o3 string + if err := rows.Scan(&key, &o2, &o3); err != nil { + return names, err + } + names = append(names, key) + } + return names, rows.Err() +} + +func getResultFromCountQuery(rows *sql.Rows) (int, error) { + counts := make([]int, 0) + for rows.Next() { + var val int + if err := rows.Scan(&val); err != nil { + return -1, err + } + counts = append(counts, val) + } + if len(counts) != 1 { + return -1, fmt.Errorf("expected 1 result, got %d", len(counts)) + } + return counts[0], nil +} + +func TestNonIndirectQueries(t *testing.T) { + type testCase struct { + description string + query string + expectedResults []string + } + var tests []testCase + tests = append(tests, testCase{ + description: "simple matching query sort ascending", + query: "filter=metadata.name~cluster-&sort=metadata.name", + expectedResults: []string{"cluster-01", "cluster-02", "cluster-bacon", "cluster-eggs"}, + }) + tests = append(tests, testCase{ + description: "simple matching query sort descending", + query: "filter=metadata.name~cluster-&sort=-metadata.name", + expectedResults: []string{"cluster-eggs", "cluster-bacon", "cluster-02", "cluster-01"}, + }) + tests = append(tests, testCase{ + description: "cluster or nervous, sort name asc", + query: "filter=metadata.name~cluster-,metadata.state.name=nervous&sort=metadata.name", + expectedResults: []string{"cluster-01", "cluster-02", "cluster-bacon", "cluster-eggs", "project-04"}, + }) + tests = append(tests, testCase{ + description: "name contains a '0', sort by state asc", + query: "filter=metadata.name~0&sort=metadata.state.name,metadata.name", + expectedResults: []string{"before-local-01", "cluster-01", "project-02", "project-05", "user-01", "zebra-01", "cluster-02", "project-03", "project-01", "project-04"}, + }) + tests = append(tests, testCase{ + description: "label contains a fcio/cattleId', sort by state desc only", + query: "filter=metadata.labels[field.cattle.io/projectId]&sort=-metadata.state.name", + expectedResults: []string{"cattle-pears", "cluster-bacon", "cattle-limes", "cluster-eggs", + "cattle-lemons", "cattle-mangoes", "fleet-local", "fleet-default", "default"}, + }) + tests = append(tests, testCase{ + description: "label contains a fcio/cattleId', sort by state desc only, name asc", + query: "filter=metadata.labels[field.cattle.io/projectId]&sort=-metadata.state.name,metadata.name", + expectedResults: []string{"cattle-pears", "cluster-bacon", "cattle-limes", "cluster-eggs", + "cattle-lemons", "cattle-mangoes", "default", "fleet-default", "fleet-local"}, + }) + tests = append(tests, testCase{ + description: "label contains a fcio/cattleId', sort by state desc only, name desc", + query: "filter=metadata.labels[field.cattle.io/projectId]&sort=-metadata.state.name,-metadata.name", + expectedResults: []string{"cluster-bacon", "cattle-pears", "cluster-eggs", "cattle-limes", "fleet-local", "fleet-default", "default", "cattle-mangoes", "cattle-lemons"}, + }) + tests = append(tests, testCase{ + description: "label contains a fcio/cattleId, age between 206 and 210 (using set notation)', sort by state desc only, name desc", + query: "filter=metadata.fields[2] in (206, 207, 208, 209),metadata.fields[2]=210&filter=metadata.fields[2]<211&filter=metadata.labels[field.cattle.io/projectId]&sort=-metadata.state.name,-metadata.name", + expectedResults: []string{"cluster-bacon", "cattle-limes", "cattle-mangoes"}, + }) + // This is commented out because there's an off-by-one error involving doing '< 211'. + //tests = append(tests, testCase{ + // description: "label contains a fcio/cattleId, age between 206 and 210', sort by state desc only, name desc", + // query: "filter=metadata.fields[2]>205&filter=metadata.fields[2]<211&filter=metadata.labels[field.cattle.io/projectId]&sort=-metadata.state.name,-metadata.name", + // expectedResults: []string{"cluster-bacon", "cattle-limes", "cattle-mangoes"}, + //}) + //tests = append(tests, testCase{ + // description: "TEMP TEST: fields[2] 206 - 208", + // query: "filter=metadata.fields[2]>205&filter=metadata.fields[2]<209&sort=metadata.fields[2]", + // expectedResults: []string{"cattle-mangoes", "cattle-limes", "cattle-kiwis"}, + //}) + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + rows, err := wrapStartOfQueryGetRows(t, context.Background(), test.query) + require.Nil(t, err) + names, err := getFirstFieldFromRows(rows) + require.Nil(t, err) + assert.Equal(t, len(test.expectedResults), len(names)) + assert.Equal(t, test.expectedResults, names) + }) + } +} + +func TestSimpleIndirectQueries(t *testing.T) { + type testCase struct { + description string + query string + expectedResults []string + } + var tests []testCase + tests = append(tests, testCase{ + description: "indirect on cluster-*, accepting all, ASC", + query: "filter=metadata.name~cluster-&sort=metadata.labels[field.cattle.io/projectId]=>[management.cattle.io/v3][Project][metadata.name][spec.clusterName]", + expectedResults: []string{"cluster-eggs", "cluster-bacon", "cluster-01", "cluster-02"}, + }) + tests = append(tests, testCase{ + description: "indirect on cluster-*, accepting all, DESC (so nulls first)", + query: "filter=metadata.name~cluster-&sort=-metadata.labels[field.cattle.io/projectId]=>[management.cattle.io/v3][Project][metadata.name][spec.clusterName]", + expectedResults: []string{"cluster-01", "cluster-02", "cluster-bacon", "cluster-eggs"}, + }) + tests = append(tests, testCase{ + description: "label contains a fcio/cattleId, age between 206 and 210 (using set notation)', indirect sort by state desc only, name desc", + query: "filter=metadata.fields[2] in (206, 207, 208, 209),metadata.fields[2]=210&filter=metadata.fields[2]<211&filter=metadata.labels[field.cattle.io/projectId]&sort=metadata.labels[field.cattle.io/projectId]=>[management.cattle.io/v3][Project][metadata.name][spec.clusterName],-metadata.name", + expectedResults: []string{"cattle-limes", "cluster-bacon", "cattle-mangoes"}, + }) + tests = append(tests, testCase{ + description: "label contains a fcio/cattleId, age between 206 and 210 (using set notation)', indirect sort by state desc only, name desc, redundant label accessors", + query: "filter=metadata.fields[2] in (206, 207, 208, 209, 210)&filter=metadata.labels[field.cattle.io/projectId]&filter=metadata.labels[field.cattle.io/projectId]&sort=metadata.labels[field.cattle.io/projectId]=>[management.cattle.io/v3][Project][metadata.name][spec.clusterName],-metadata.name", + expectedResults: []string{"cattle-limes", "cluster-bacon", "cattle-mangoes"}, + }) + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + ctx := context.Background() + rows, err := wrapStartOfQueryGetRows(t, ctx, test.query) + require.Nil(t, err) + names, err := getFirstFieldFromRows(rows) + require.Nil(t, err) + assert.Equal(t, len(test.expectedResults), len(names)) + assert.Equal(t, test.expectedResults, names) + }) + } +} + +func TestMultiSortWithIndirect(t *testing.T) { + type testCase struct { + description string + query string + expectedResults []string + } + var tests []testCase + tests = append(tests, testCase{ + description: "indirect on cluster-*, accepting all, ASC", + query: "filter=metadata.name~cluster-,metadata.name~cattle-&sort=metadata.state.name,metadata.labels[field.cattle.io/projectId]=>[management.cattle.io/v3][Project][metadata.name][spec.clusterName],metadata.name", + expectedResults: []string{ + // state: "active" + // cluster-01 clusterName + "cattle-lemons", + // local clusterName + "cattle-mangoes", + // no clusterName + "cattle-kiwis", + "cattle-plums", + "cluster-01", + + // state: "hungry" + // cluster-01 clusterName + "cluster-eggs", + // cluster-02 clusterName + "cattle-limes", + // no clusterName + "cluster-02", + + // state: "muddy" + // local clusterName + "cattle-pears", + "cluster-bacon", + }, + }) + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + ctx := context.Background() + l, lo, err := getListOptionIndexerForQuery(t, ctx, test.query) + require.Nil(t, err) + p := partition.Partition{Passthrough: true} + partitions := []partition.Partition{p} + namespace := "" + queryInfo, err := l.constructQuery(lo, partitions, namespace, "_v1_Namespace") + require.Nil(t, err) + stmt, err := dbClient.Prepare(queryInfo.query) + require.Nil(t, err) + defer stmt.Close() + rows, err := stmt.Query(queryInfo.params...) + require.Nil(t, err) + names, err := getFirstFieldFromRows(rows) + require.Nil(t, err) + assert.Equal(t, len(test.expectedResults), len(names)) + assert.Equal(t, test.expectedResults, names) + }) + } +} + +func TestIndirectFilteringOnANonLabelLink(t *testing.T) { + type testCase struct { + description string + query string + expectedResults []string + } + var tests []testCase + tests = append(tests, testCase{ + description: "indirect filter on namespace.state = foods_fields[state].country = japan", + query: "filter=metadata.fields[2]=>[_v1][Foods][foodCode][country]=japan&sort=metadata.name", + expectedResults: []string{ + "before-local-01", "cattle-lemons", "default", "project-04", "project-05", + }, + }) + tests = append(tests, testCase{ + description: "indirect filter on hungry clusters based on foods from canada", + query: "filter=metadata.fields[2]=>[_v1][Foods][foodCode][country]=canada&sort=metadata.name&filter=metadata.fields[2]=>[_v1][Foods][foodCode][state]=hungry", + expectedResults: []string{ + "cluster-02", "cluster-eggs", "project-03", + }, + }) + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + ctx := context.Background() + rows, err := wrapStartOfQueryGetRows(t, ctx, test.query) + require.Nil(t, err) + names, err := getFirstFieldFromRows(rows) + require.Nil(t, err) + assert.Equal(t, len(test.expectedResults), len(names)) + assert.Equal(t, test.expectedResults, names) + }) + } + +} + +func TestIndirectSortingOnANonLabelLink(t *testing.T) { + type testCase struct { + description string + query string + expectedResults []string + } + var tests []testCase + tests = append(tests, testCase{ + description: "indirect sort on namespace.state = foods_fields[state].country, select hungries", + query: "sort=metadata.fields[2]=>[_v1][Foods][foodCode][country],metadata.name&filter=metadata.state.name=hungry", + expectedResults: []string{ + // canada + "cluster-02", + "cluster-eggs", + "project-03", + // france + "local", + // mexico + "cattle-limes", + }, + }) + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + ctx := context.Background() + rows, err := wrapStartOfQueryGetRows(t, ctx, test.query) + require.Nil(t, err) + names, err := getFirstFieldFromRows(rows) + require.Nil(t, err) + assert.Equal(t, len(test.expectedResults), len(names)) + assert.Equal(t, test.expectedResults, names) + }) + } +} + +func TestPagination(t *testing.T) { + type testCase struct { + description string + queryTemplate string + expectedResults [][]string + } + var tests []testCase + tests = append(tests, testCase{ + description: "no sorting, no filter, 3 per page", + queryTemplate: "page=%d&pagesize=3", + expectedResults: [][]string{ + {"before-local-01", + "cattle-kiwis", + "cattle-lemons"}, + {"cattle-limes", + "cattle-mangoes", + "cattle-pears"}, + {"cattle-plums", + "cluster-01", + "cluster-02"}, + {"cluster-bacon", + "cluster-eggs", + "default"}, + {"fleet-default", + "fleet-local", + "local"}, + {"project-01", + "project-02", + "project-03"}, + {"project-04", + "project-05", + "user-01"}, + { + "zebra-01", + }, + }, + }) + tests = append(tests, testCase{ + description: "sort by state, no filter, 5 per page", + queryTemplate: "page=%d&pagesize=5&sort=metadata.state.name", + expectedResults: [][]string{ + { + "cluster-01", + "cattle-lemons", + "cattle-mangoes", + "cattle-kiwis", + "cattle-plums", + }, { + "default", + "fleet-default", + "fleet-local", + "before-local-01", + "project-02", + }, { + "project-05", + "user-01", + "zebra-01", + "cluster-02", + "cattle-limes", + }, { + "cluster-eggs", + "local", + "project-03", + "cattle-pears", + "cluster-bacon", + }, { + "project-01", + "project-04", + }, + }, + }) + tests = append(tests, testCase{ + description: "external sort by country via foodCode, no filter, 4 per page", + queryTemplate: "page=%d&pagesize=4&sort=metadata.fields[2] => [_v1][Foods][foodCode][country]", + expectedResults: [][]string{ + { + "cluster-01", + "cluster-02", + "cattle-pears", + "cattle-plums", + }, { + "cluster-bacon", + "cluster-eggs", + "project-03", + "fleet-local", + }, { + "local", + "project-01", + "cattle-lemons", + "default", + }, { + "before-local-01", + "project-04", + "project-05", + "cattle-mangoes", + }, { + "cattle-limes", + "fleet-default", + "cattle-kiwis", + "project-02", + }, + }, + }) + tests = append(tests, testCase{ + description: "external sort active-only by country via foodCode, 7 per page", + queryTemplate: "page=%d&pagesize=7&filter=metadata.state.name=active&sort=metadata.fields[2] => [_v1][Foods][foodCode][country]", + expectedResults: [][]string{ + { + "cluster-01", + "cattle-plums", + "fleet-local", + "cattle-lemons", + "default", + "before-local-01", + "project-05", + }, { + "cattle-mangoes", + "fleet-default", + "cattle-kiwis", + "project-02", + }, + }, + }) + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + ctx := context.Background() + p := partition.Partition{Passthrough: true} + partitions := []partition.Partition{p} + namespace := "" + numTotal := 0 + for _, part := range test.expectedResults { + numTotal += len(part) + } + for i, part := range test.expectedResults { + query := fmt.Sprintf(test.queryTemplate, i+1) + l, lo, err := getListOptionIndexerForQuery(t, ctx, query) + require.Nil(t, err) + queryInfo, err := l.constructQuery(lo, partitions, namespace, "_v1_Namespace") + require.Nil(t, err) + rows, err := func() (*sql.Rows, error) { + stmt, err := dbClient.Prepare(queryInfo.query) + require.Nil(t, err) + defer stmt.Close() + return stmt.Query(queryInfo.params...) + }() + require.Nil(t, err) + names, err := getFirstFieldFromRows(rows) + require.Nil(t, err) + assert.Equal(t, len(part), len(names)) + assert.Equal(t, part, names) + if i == len(test.expectedResults)-1 { + // Verify that we've seen everything we expected to see + rows, err := func() (*sql.Rows, error) { + stmt, err := dbClient.Prepare(queryInfo.countQuery) + require.Nil(t, err) + defer stmt.Close() + return stmt.Query(queryInfo.countParams...) + }() + require.Nil(t, err) + var fullCount int + fullCount, err = getResultFromCountQuery(rows) + require.Nil(t, err) + assert.Equal(t, numTotal, fullCount) + } + } + }) + } +} + +func TestMain(m *testing.M) { + err := setupTests() + if err != nil { + panic(fmt.Sprintf("Awp! verify_generator_test.go tests failed to setup: %s", err)) + } + m.Run() + err = teardownTests() + if err != nil { + fmt.Fprintf(os.Stderr, "teardown tests failed: %s\n", err) + } +} + +func setupTests() error { + var err error + vaiFile, err = ioutil.TempFile("", "vaidb") + if err != nil { + return err + } + db, err := sql.Open("sqlite3", vaiFile.Name()) + if err != nil { + return err + } + dbClient = db + _, filename, _, ok := runtime.Caller(0) + if !ok { + return errors.New("test setup: runtime.Caller() failed") + } + fixtureDir := filepath.Join(filepath.Dir(filename), "fixtures") + fileNames := []string{ + "schema.txt", "_v1_Namespace.txt", "_v1_Namespace_fields.txt", + "_v1_Namespace_labels.txt", "management.cattle.io_v3_Project.txt", + "management.cattle.io_v3_Project_fields.txt", + "management.cattle.io_v3_Project_labels.txt", + "_v1_Foods.txt", "_v1_Foods_fields.txt", + "_v1_Foods_labels.txt", + } + for _, fileName := range fileNames { + fullPath := filepath.Join(fixtureDir, fileName) + sqlStmt, err := ioutil.ReadFile(fullPath) + if err != nil { + return err + } + if len(sqlStmt) == 0 { + continue + } + _, err = db.Exec(string(sqlStmt)) + if err != nil { + return fmt.Errorf("setup: can't create execute file %s: %w", fullPath, err) + } + } + return nil +} + +func teardownTests() error { + if dbClient != nil { + if err := dbClient.Close(); err != nil { + return err + } + } + if vaiFile != nil { + return os.Remove(vaiFile.Name()) + } + return nil +}