From b3c84aab65ed0705b6911294704254b1b2bdd0d0 Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Tue, 7 Feb 2023 21:09:25 -0500 Subject: [PATCH] add update graph tests --- .github/workflows/build-test.yaml | 19 ++ .gitignore | 2 + .golangci.yaml | 4 + .yamllint | 3 +- CONTRIBUTING.md | 8 +- Dockerfile | 2 +- Dockerfile.release | 2 +- e2e/cluster_test.go | 270 ++++++++++-------- e2e/databases/cockroach.go | 122 ++++++++ e2e/databases/database.go | 74 +++++ .../manifests}/cockroachdb.yaml | 0 .../manifests}/mysql.yaml | 0 .../manifests}/postgres.yaml | 13 + .../manifests}/spanner.yaml | 0 e2e/databases/mysql.go | 130 +++++++++ e2e/databases/postgres.go | 125 ++++++++ e2e/databases/spanner.go | 184 ++++++++++++ e2e/e2e_test.go | 64 ++++- e2e/util/port_forward.go | 74 +++++ e2e/util_test.go | 114 ++------ go.mod | 8 +- go.sum | 4 + goreleaser.yaml | 4 +- magefiles/magefile.go | 92 +++++- pkg/crds/authzed.com_spicedbclusters.yaml | 32 ++- pkg/updates/file.go | 1 - ...-config.yaml => proposed-update-graph.yaml | 231 --------------- tools/generate-update-graph/main.go | 92 ++---- tools/go.mod | 3 - tools/go.sum | 7 - tools/validate-graph/main.go | 119 -------- validated-update-graph.yaml | 2 + 32 files changed, 1121 insertions(+), 684 deletions(-) create mode 100644 e2e/databases/cockroach.go create mode 100644 e2e/databases/database.go rename e2e/{manifests/datastores => databases/manifests}/cockroachdb.yaml (100%) rename e2e/{manifests/datastores => databases/manifests}/mysql.yaml (100%) rename e2e/{manifests/datastores => databases/manifests}/postgres.yaml (78%) rename e2e/{manifests/datastores => databases/manifests}/spanner.yaml (100%) create mode 100644 e2e/databases/mysql.go create mode 100644 e2e/databases/postgres.go create mode 100644 e2e/databases/spanner.go create mode 100644 e2e/util/port_forward.go rename default-operator-config.yaml => proposed-update-graph.yaml (75%) delete mode 100644 tools/validate-graph/main.go create mode 100644 validated-update-graph.yaml diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index a952da02..bab8ace9 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -101,6 +101,25 @@ jobs: with: version: "latest" args: "test:e2e" + - name: "Check if validated update graph has changed" + uses: "tj-actions/verify-changed-files@v13" + id: "verify-changed-graph" + with: + files: | + validated-update-graph.yaml + - name: "Commit validated update graph" + uses: "EndBug/add-and-commit@v9" + if: | + steps.verify-changed-graph.outputs.files_changed == 'true' + with: + author_name: "Authzed, Inc." + author_email: "86801627+authzedbot@users.noreply.github.com" + committer_name: "GitHub Actions" + committer_email: "41898282+github-actions[bot]@users.noreply.github.com" + message: "update validated graph after successful tests" + pathspec_error_handling: "exitImmediately" + push: true + tag: "${{github.ref_name}}" - uses: "actions/upload-artifact@v2" if: "always()" # this upload step is really flaky, don't fail the job if it fails diff --git a/.gitignore b/.gitignore index 8428a4e6..48f22585 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ release/ *.kubeconfig *.test e2e/cluster-state/** +e2e/*.lck +e2e/*.lck.* diff --git a/.golangci.yaml b/.golangci.yaml index 98d05b6b..28f3b677 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -6,6 +6,10 @@ output: linters-settings: goimports: local-prefixes: "github.com/authzed/spicedb-operator" + stylecheck: + dot-import-whitelist: + - "github.com/onsi/ginkgo/v2" + - "github.com/onsi/gomega" linters: enable: - "bidichk" diff --git a/.yamllint b/.yamllint index fd06ae43..a91e3f22 100644 --- a/.yamllint +++ b/.yamllint @@ -2,7 +2,8 @@ --- ignore: | config/ - default-operator-config.yaml + proposed-update-graph.yaml + validated-update-graph.yaml e2e/ examples pkg/crds diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e394dcc..017a5cf2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,11 +105,11 @@ go get github.com/org/newdependency@version Continuous integration enforces that `go mod tidy` has been run. -### Regenerating `default-operator-config.yaml` +### Regenerating `proposed-update-graph.yaml` -The default config can be regenerated whenever there is a new spicedb release. +The update graph can be regenerated whenever there is a new spicedb release. +CI will validate all new edges when there are changes to `proposed-update-graph.yaml` and will copy them into `validated-update-graph.yaml` if successful. ```go -cd tools -go generate ./... +mage gen:graph ``` diff --git a/Dockerfile b/Dockerfile index 54d5a81b..11f009d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,6 @@ RUN go build ./cmd/... FROM alpine:3.17.0 -COPY --from=builder /go/src/app/default-operator-config.yaml /opt/operator/config.yaml +COPY --from=builder /go/src/app/validated-update-graph.yaml /opt/operator/config.yaml COPY --from=builder /go/src/app/spicedb-operator /usr/local/bin/spicedb-operator ENTRYPOINT ["spicedb-operator"] diff --git a/Dockerfile.release b/Dockerfile.release index 8b3d61a1..5926dc9e 100644 --- a/Dockerfile.release +++ b/Dockerfile.release @@ -1,6 +1,6 @@ # vim: syntax=dockerfile FROM gcr.io/distroless/base -COPY default-operator-config.yaml /opt/operator/config.yaml +COPY validated-update-graph.yaml /opt/operator/config.yaml COPY --from=ghcr.io/grpc-ecosystem/grpc-health-probe:v0.4.12 /ko-app/grpc-health-probe /usr/local/bin/grpc_health_probe COPY spicedb-operator /usr/local/bin/spicedb-operator ENTRYPOINT ["spicedb-operator"] diff --git a/e2e/cluster_test.go b/e2e/cluster_test.go index 11e18c45..3a56f06c 100644 --- a/e2e/cluster_test.go +++ b/e2e/cluster_test.go @@ -10,18 +10,13 @@ import ( "os" "time" - database "cloud.google.com/go/spanner/admin/database/apiv1" - instances "cloud.google.com/go/spanner/admin/instance/apiv1" "github.com/authzed/controller-idioms/typed" "github.com/go-logr/logr" "github.com/go-logr/logr/funcr" - "github.com/jackc/pgx/v5" "github.com/jzelinskie/stringz" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/types" - adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1" - "google.golang.org/genproto/googleapis/spanner/admin/instance/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" @@ -35,8 +30,10 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/restmapper" + "github.com/authzed/spicedb-operator/e2e/databases" "github.com/authzed/spicedb-operator/pkg/apis/authzed/v1alpha1" "github.com/authzed/spicedb-operator/pkg/metadata" + "github.com/authzed/spicedb-operator/pkg/updates" ) var ( @@ -103,9 +100,10 @@ var _ = Describe("SpiceDBClusters", func() { } config := map[string]any{ - "envPrefix": spicedbEnvPrefix, - "image": image, - "cmd": spicedbCmd, + "envPrefix": spicedbEnvPrefix, + "image": image, + "cmd": spicedbCmd, + "skipReleaseCheck": "true", } jsonConfig, err := json.Marshal(config) Expect(err).To(Succeed()) @@ -221,29 +219,26 @@ var _ = Describe("SpiceDBClusters", func() { }) Describe("With a database", func() { - var ( - datastoreURI string - engine string - extraConfig map[string]string - ) + var db *databases.LogicalDatabase BasicSpiceDBFunctionality := func() { When("a valid SpiceDBCluster is created", func() { BeforeEach(func() { secret.StringData = map[string]string{ "logLevel": "debug", - "datastore_uri": datastoreURI, + "datastore_uri": db.DatastoreURI, "preshared_key": "testtesttesttest", "migration_secrets": "kaitain-bootstrap-token=testtesttesttest,sharewith-bootstrap-token=testtesttesttest,thumper-bootstrap-token=testtesttesttest,metrics-proxy-token=testtesttesttest", } config := map[string]any{ - "datastoreEngine": engine, - "envPrefix": spicedbEnvPrefix, - "image": image, - "cmd": spicedbCmd, + "skipReleaseCheck": "true", + "datastoreEngine": db.Engine, + "envPrefix": spicedbEnvPrefix, + "image": image, + "cmd": spicedbCmd, } - for k, v := range extraConfig { + for k, v := range db.ExtraConfig { config[k] = v } jsonConfig, err := json.Marshal(config) @@ -253,7 +248,7 @@ var _ = Describe("SpiceDBClusters", func() { }) JustBeforeEach(func() { - AssertMigrationsCompleted(image, "", "", cluster.Name, engine) + AssertMigrationsCompleted(image, "", "", cluster.Name, db.Engine) }) AfterEach(func() { @@ -272,7 +267,8 @@ var _ = Describe("SpiceDBClusters", func() { // this installs from the head of the current channel, skip validating image image = "" config := map[string]any{ - "datastoreEngine": engine, + "skipReleaseCheck": true, + "datastoreEngine": db.Engine, "envPrefix": spicedbEnvPrefix, "cmd": spicedbCmd, "tlsSecretName": "spicedb-grpc-tls", @@ -280,7 +276,7 @@ var _ = Describe("SpiceDBClusters", func() { "serviceAccountName": "spicedb-non-default", "extraServiceAccountAnnotations": "authzed.com/e2e=true", } - for k, v := range extraConfig { + for k, v := range db.ExtraConfig { config[k] = v } jsonConfig, err := json.Marshal(config) @@ -319,13 +315,13 @@ var _ = Describe("SpiceDBClusters", func() { BeforeEach(func() { secret.StringData = map[string]string{ "logLevel": "debug", - "datastore_uri": datastoreURI, + "datastore_uri": db.DatastoreURI, "preshared_key": "testtesttesttest", "migration_secrets": "kaitain-bootstrap-token=testtesttesttest,sharewith-bootstrap-token=testtesttesttest,thumper-bootstrap-token=testtesttesttest,metrics-proxy-token=testtesttesttest", } config, err := json.Marshal(map[string]any{ "skipMigrations": true, - "datastoreEngine": engine, + "datastoreEngine": db.Engine, "image": image, "envPrefix": spicedbEnvPrefix, "cmd": spicedbCmd, @@ -355,55 +351,145 @@ var _ = Describe("SpiceDBClusters", func() { }) } - Describe("With cockroachdb", Ordered, func() { - BeforeAll(func() { - engine = "cockroachdb" - datastoreURI = "postgresql://root:unused@cockroachdb-public.crdb:26257/defaultdb?sslmode=disable" - CreateNamespace("crdb") - DeferCleanup(DeleteNamespace, "crdb") - CreateDatabase(ctx, mapper, "crdb", engine) + UpdateTest := func(engine, channel, from, to string) { + Describe(fmt.Sprintf("using channel %s, from %s to %s", channel, from, to), func() { + BeforeEach(func() { + secret.StringData = map[string]string{ + "logLevel": "debug", + "datastore_uri": db.DatastoreURI, + "preshared_key": "testtesttesttest", + "migration_secrets": "kaitain-bootstrap-token=testtesttesttest,sharewith-bootstrap-token=testtesttesttest,thumper-bootstrap-token=testtesttesttest,metrics-proxy-token=testtesttesttest", + } + + config := map[string]any{ + "skipReleaseCheck": "true", + "datastoreEngine": engine, + "envPrefix": spicedbEnvPrefix, + "cmd": spicedbCmd, + } + for k, v := range db.ExtraConfig { + config[k] = v + } + jsonConfig, err := json.Marshal(config) + Expect(err).To(Succeed()) + + cluster.Spec.Config = jsonConfig + cluster.Spec.Channel = channel + cluster.Spec.Version = from + }) + + JustBeforeEach(func() { + Eventually(func(g Gomega) { + clusterUnst, err := client.Resource(v1alpha1ClusterGVR).Namespace(cluster.Namespace).Get(ctx, cluster.Name, metav1.GetOptions{}) + g.Expect(err).To(Succeed()) + fetched, err := typed.UnstructuredObjToTypedObj[*v1alpha1.SpiceDBCluster](clusterUnst) + g.Expect(err).To(Succeed()) + logr.FromContextOrDiscard(ctx).Info("fetched cluster", "status", fetched.Status) + g.Expect(fetched.Status.CurrentVersion).ToNot(BeNil()) + g.Expect(fetched.Status.CurrentVersion.Name).To(Equal(from)) + meta.RemoveStatusCondition(&fetched.Status.Conditions, v1alpha1.ConditionTypeConfigWarnings) + g.Expect(len(fetched.Status.Conditions)).To(BeZero()) + g.Expect(len(fetched.Status.AvailableVersions)).ToNot(BeZero(), "status should show available updates") + // TODO: validate the target version is in the available version list + }).Should(Succeed()) + + // once the cluster is running at the initial version, update the target version + Eventually(func(g Gomega) { + clusterUnst, err := client.Resource(v1alpha1ClusterGVR).Namespace(cluster.Namespace).Get(ctx, cluster.Name, metav1.GetOptions{}) + g.Expect(err).To(Succeed()) + fetched, err := typed.UnstructuredObjToTypedObj[*v1alpha1.SpiceDBCluster](clusterUnst) + g.Expect(err).To(Succeed()) + meta.RemoveStatusCondition(&fetched.Status.Conditions, v1alpha1.ConditionTypeConfigWarnings) + g.Expect(len(fetched.Status.Conditions)).To(BeZero()) + + fetched.Spec.Version = to + u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fetched) + g.Expect(err).To(Succeed()) + _, err = client.Resource(v1alpha1ClusterGVR).Namespace(cluster.Namespace).Update(ctx, &unstructured.Unstructured{Object: u}, metav1.UpdateOptions{}) + g.Expect(err).To(Succeed()) + }).Should(Succeed()) + }) + + AfterEach(func() { + AssertDependentResourceCleanup(cluster.Name, "spicedb") + }) + + It("should update with no issues", func() { + Eventually(func(g Gomega) { + clusterUnst, err := client.Resource(v1alpha1ClusterGVR).Namespace(cluster.Namespace).Get(ctx, cluster.Name, metav1.GetOptions{}) + g.Expect(err).To(Succeed()) + fetched, err := typed.UnstructuredObjToTypedObj[*v1alpha1.SpiceDBCluster](clusterUnst) + g.Expect(err).To(Succeed()) + logr.FromContextOrDiscard(ctx).Info("fetched cluster", "status", fetched.Status) + g.Expect(fetched.Status.CurrentVersion).ToNot(BeNil()) + meta.RemoveStatusCondition(&fetched.Status.Conditions, v1alpha1.ConditionTypeConfigWarnings) + g.Expect(len(fetched.Status.Conditions)).To(BeZero()) + g.Expect(fetched.Status.CurrentVersion.Name).To(Equal(to)) + }).Should(Succeed()) + + AssertHealthySpiceDBCluster("", cluster.Name, Not(ContainSubstring("ERROR: kuberesolver"))) + }) + }) + } + + ValidateNewGraphEdges := func(engine string) { + Describe("with a new update graph", func() { + proposedGraph := GetConfig(ProposedGraphFile) + validatedGraph := GetConfig(ValidatedGraphFile) + diffGraph := proposedGraph.UpdateGraph.Difference(&validatedGraph.UpdateGraph) + for _, c := range diffGraph.Channels { + if c.Metadata[updates.DatastoreMetadataKey] == engine { + for source, targets := range c.Edges { + source := source + for _, target := range targets { + target := target + UpdateTest(engine, c.Name, source, target) + } + } + } + } + }) + } + + Describe("With cockroachdb", func() { + BeforeEach(func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + db = crdbProvider.New(ctx) + DeferCleanup(crdbProvider.Cleanup, context.Background(), db) }) BasicSpiceDBFunctionality() + ValidateNewGraphEdges("cockroachdb") }) - Describe("With mysql", Ordered, func() { - BeforeAll(func() { - engine = "mysql" - datastoreURI = "root:password@tcp(mysql-public.mysql:3306)/mysql?parseTime=true" - CreateNamespace("mysql") - DeferCleanup(DeleteNamespace, "mysql") - CreateDatabase(ctx, mapper, "mysql", engine) + Describe("With mysql", func() { + BeforeEach(func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + db = mysqlProvider.New(ctx) + DeferCleanup(mysqlProvider.Cleanup, context.Background(), db) }) BasicSpiceDBFunctionality() + ValidateNewGraphEdges("mysql") }) - Describe("With postgres", Ordered, func() { - BeforeAll(func() { - engine = "postgres" - datastoreURI = "postgresql://postgres:testpassword@postgresql-db-public.postgres:5432/postgres?sslmode=disable" - CreateNamespace("postgres") - DeferCleanup(DeleteNamespace, "postgres") - CreateDatabase(ctx, mapper, "postgres", engine) + Describe("With postgres", func() { + BeforeEach(func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + db = postgresProvider.New(ctx) + DeferCleanup(postgresProvider.Cleanup, context.Background(), db) }) BasicSpiceDBFunctionality() + ValidateNewGraphEdges("postgres") Describe("there is a series of required migrations", func() { BeforeEach(func() { - func() { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - PortForward("postgres", "postgresql-db-0", []string{"5432"}, ctx.Done()) - conn, err := pgx.Connect(ctx, "postgresql://postgres:testpassword@localhost:5432/postgres?sslmode=disable") - Expect(err).To(Succeed()) - _, err = conn.Exec(ctx, "CREATE DATABASE postgres2;") - Expect(err).To(Succeed()) - datastoreURI = "postgresql://postgres:testpassword@postgresql-db-public.postgres:5432/postgres2?sslmode=disable" - }() - classConfig := map[string]any{ + "skipReleaseCheck": "true", "logLevel": "debug", "datastoreEngine": "postgres", "tlsSecretName": "spicedb-grpc-tls", @@ -415,7 +501,7 @@ var _ = Describe("SpiceDBClusters", func() { cluster.Spec.Config = jsonConfig secret.StringData = map[string]string{ - "datastore_uri": datastoreURI, + "datastore_uri": db.DatastoreURI, "migration_secrets": "kaitain-bootstrap-token=testtesttesttest,sharewith-bootstrap-token=testtesttesttest,thumper-bootstrap-token=testtesttesttest,metrics-proxy-token=testtesttesttest", "preshared_key": "testtesttesttest", } @@ -478,73 +564,21 @@ var _ = Describe("SpiceDBClusters", func() { }) }) - Describe("With spanner", Ordered, func() { - BeforeAll(func() { - engine = "spanner" - datastoreURI = "projects/fake-project-id/instances/fake-instance/databases/fake-database-id" - extraConfig = map[string]string{ - "datastoreSpannerEmulatorHost": "spanner-service.spanner:9010", - } - CreateNamespace("spanner") - DeferCleanup(DeleteNamespace, "spanner") - CreateDatabase(ctx, mapper, "spanner", engine) - + Describe("With spanner", func() { + BeforeEach(func() { ctx, cancel := context.WithCancel(context.Background()) - PortForward("spanner", "spanner-0", []string{"9010"}, ctx.Done()) defer cancel() - Expect(os.Setenv("SPANNER_EMULATOR_HOST", "localhost:9010")).To(Succeed()) - - var instancesClient *instances.InstanceAdminClient - Eventually(func() *instances.InstanceAdminClient { - // Create instance - client, err := instances.NewInstanceAdminClient(ctx) - if err != nil { - return nil - } - instancesClient = client - return client - }).Should(Not(BeNil())) - - defer func() { Expect(instancesClient.Close()).To(Succeed()) }() - - var createInstanceOp *instances.CreateInstanceOperation - Eventually(func(g Gomega) { - var err error - createInstanceOp, err = instancesClient.CreateInstance(ctx, &instance.CreateInstanceRequest{ - Parent: "projects/fake-project-id", - InstanceId: "fake-instance", - Instance: &instance.Instance{ - Config: "emulator-config", - DisplayName: "Test Instance", - NodeCount: 1, - }, - }) - g.Expect(err).To(Succeed()) - }).Should(Succeed()) - - spannerInstance, err := createInstanceOp.Wait(ctx) - Expect(err).To(Succeed()) - - // Create db - adminClient, err := database.NewDatabaseAdminClient(ctx) - Expect(err).To(Succeed()) - defer func() { - Expect(adminClient.Close()).To(Succeed()) - }() - - dbID := "fake-database-id" - op, err := adminClient.CreateDatabase(ctx, &adminpb.CreateDatabaseRequest{ - Parent: spannerInstance.Name, - CreateStatement: "CREATE DATABASE `" + dbID + "`", - }) - Expect(err).To(Succeed()) - - _, err = op.Wait(ctx) - Expect(err).To(Succeed()) + // Each spanner test spins up its own database pod; the spanner + // emulator doesn't support concurrent transactions so a single + // instance can't be shared. + spannerProvider := databases.NewSpannerProvider(mapper, restConfig, testNamespace) + db = spannerProvider.New(ctx) + DeferCleanup(spannerProvider.Cleanup, context.Background(), db) }) BasicSpiceDBFunctionality() + ValidateNewGraphEdges("spanner") }) }) }) diff --git a/e2e/databases/cockroach.go b/e2e/databases/cockroach.go new file mode 100644 index 00000000..d4db99b4 --- /dev/null +++ b/e2e/databases/cockroach.go @@ -0,0 +1,122 @@ +package databases + +import ( + "context" + "fmt" + "path/filepath" + "time" + + "github.com/go-logr/logr" + "github.com/jackc/pgx/v5" + "github.com/nightlyone/lockfile" + + //revive:disable:dot-imports convention is dot-import + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/storage/names" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + e2eutil "github.com/authzed/spicedb-operator/e2e/util" +) + +const CRDBAdminConnString = "postgresql://root:unused@localhost:%d/defaultdb?sslmode=disable" + +type CockroachProvider struct { + kclient kubernetes.Interface + namespace string + lock lockfile.Lockfile + mapper meta.RESTMapper + restConfig *rest.Config +} + +func NewCockroachProvider(mapper meta.RESTMapper, restConfig *rest.Config, namespace string) *CockroachProvider { + k, err := kubernetes.NewForConfig(restConfig) + Expect(err).To(Succeed()) + lockPath, err := filepath.Abs("./crdb.lck") + Expect(err).To(Succeed()) + lock, err := lockfile.New(lockPath) + Expect(err).To(Succeed()) + return &CockroachProvider{kclient: k, namespace: namespace, lock: lock, mapper: mapper, restConfig: restConfig} +} + +func (p *CockroachProvider) New(ctx context.Context) *LogicalDatabase { + p.ensureDatabase(ctx) + var logicalDBName string + Eventually(func(g Gomega) { + logicalDBName = names.SimpleNameGenerator.GenerateName("crdb") + p.exec(ctx, g, fmt.Sprintf("CREATE DATABASE %s;", logicalDBName)) + }).Should(Succeed()) + return &LogicalDatabase{ + DatastoreURI: fmt.Sprintf("postgresql://root:unused@cockroachdb-public.%s:26257/%s?sslmode=disable", p.namespace, logicalDBName), + DatabaseName: logicalDBName, + Engine: "cockroachdb", + } +} + +func (p *CockroachProvider) Cleanup(ctx context.Context, db *LogicalDatabase) { + p.exec(ctx, Default, fmt.Sprintf("DROP DATABASE %s;", db.DatabaseName)) +} + +func (p *CockroachProvider) exec(ctx context.Context, g Gomega, cmd string) { + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + ports := e2eutil.PortForward(g, p.namespace, "cockroachdb-0", []string{":26257"}, ctx.Done()) + g.Expect(len(ports)).To(Equal(1)) + conn, err := pgx.Connect(ctx, fmt.Sprintf(CRDBAdminConnString, ports[0].Local)) + if err != nil { + GinkgoWriter.Println(err) + } + + g.Expect(err).To(Succeed()) + _, err = conn.Exec(ctx, cmd) + if err != nil { + GinkgoWriter.Println(err) + } + g.Expect(err).To(Succeed()) +} + +// ensureDatabase checks to see if the cockroach instance has been set up, +// and if not, creates it under a lock +func (p *CockroachProvider) ensureDatabase(ctx context.Context) { + logger := logr.FromContextOrDiscard(ctx) + + for { + if p.running(ctx) == nil { + logger.V(4).Info("crdb found running") + return + } + logger.V(2).Info("crdb not running, attempting to acquire lock") + if err := p.lock.TryLock(); err != nil { + logger.V(2).Info("cannot acquire lock, retry") + continue + } + defer func() { + Expect(p.lock.Unlock()).To(Succeed()) + logger.V(4).Info("crdb lock released, creating database") + }() + logger.V(2).Info("crdb lock acquired, creating database") + + CreateFromManifests(ctx, p.namespace, "cockroachdb", p.restConfig, p.mapper) + + Eventually(func(g Gomega) { + g.Expect(p.running(ctx)).To(Succeed()) + }).Should(Succeed()) + return + } +} + +func (p *CockroachProvider) running(ctx context.Context) error { + if _, err := p.kclient.CoreV1().Namespaces().Get(ctx, p.namespace, metav1.GetOptions{}); err != nil { + return err + } + + if _, err := p.kclient.AppsV1().StatefulSets(p.namespace).Get(ctx, "cockroachdb", metav1.GetOptions{}); err != nil { + return err + } + + return nil +} diff --git a/e2e/databases/database.go b/e2e/databases/database.go new file mode 100644 index 00000000..a8702d4b --- /dev/null +++ b/e2e/databases/database.go @@ -0,0 +1,74 @@ +package databases + +import ( + "context" + "embed" + "errors" + "fmt" + "io" + "time" + + "github.com/fluxcd/pkg/ssa" + //revive:disable:dot-imports convention is dot-import + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/rest" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling" + crClient "sigs.k8s.io/controller-runtime/pkg/client" +) + +//go:embed manifests/*.yaml +var datastores embed.FS + +type LogicalDatabase struct { + Engine string + DatastoreURI string + DatabaseName string + ExtraConfig map[string]string +} + +type Provider interface { + New(ctx context.Context) *LogicalDatabase + Cleanup(ctx context.Context, db *LogicalDatabase) +} + +func CreateFromManifests(ctx context.Context, namespace, engine string, restConfig *rest.Config, mapper meta.RESTMapper) { + ssaClient, err := crClient.NewWithWatch(restConfig, crClient.Options{ + Mapper: mapper, + }) + Expect(err).To(Succeed()) + resourceManager := ssa.NewResourceManager(ssaClient, polling.NewStatusPoller(ssaClient, mapper, polling.Options{}), ssa.Owner{ + Field: "test.authzed.com", + Group: "test.authzed.com", + }) + + yamlReader, err := datastores.Open(fmt.Sprintf("manifests/%s.yaml", engine)) + Expect(err).To(Succeed()) + DeferCleanup(yamlReader.Close) + + decoder := yaml.NewYAMLToJSONDecoder(yamlReader) + objs := make([]*unstructured.Unstructured, 0) + for { + u := &unstructured.Unstructured{Object: map[string]interface{}{}} + err := decoder.Decode(&u.Object) + if errors.Is(err, io.EOF) { + break + } else { + Expect(err).To(Succeed()) + } + u.SetNamespace(namespace) + objs = append(objs, u) + } + _, err = resourceManager.ApplyAll(ctx, objs, ssa.DefaultApplyOptions()) + Expect(err).To(Succeed()) + By(fmt.Sprintf("waiting for %s to start..", engine)) + err = resourceManager.Wait(objs, ssa.WaitOptions{ + Interval: 1 * time.Second, + Timeout: 120 * time.Second, + }) + Expect(err).To(Succeed()) + By(fmt.Sprintf("%s running", engine)) +} diff --git a/e2e/manifests/datastores/cockroachdb.yaml b/e2e/databases/manifests/cockroachdb.yaml similarity index 100% rename from e2e/manifests/datastores/cockroachdb.yaml rename to e2e/databases/manifests/cockroachdb.yaml diff --git a/e2e/manifests/datastores/mysql.yaml b/e2e/databases/manifests/mysql.yaml similarity index 100% rename from e2e/manifests/datastores/mysql.yaml rename to e2e/databases/manifests/mysql.yaml diff --git a/e2e/manifests/datastores/postgres.yaml b/e2e/databases/manifests/postgres.yaml similarity index 78% rename from e2e/manifests/datastores/postgres.yaml rename to e2e/databases/manifests/postgres.yaml index 95d829a3..02aa02d6 100644 --- a/e2e/manifests/datastores/postgres.yaml +++ b/e2e/databases/manifests/postgres.yaml @@ -25,6 +25,12 @@ spec: volumeMounts: - name: postgresql-db-disk mountPath: /data + - name: init + mountPath: /docker-entrypoint-initdb.d + volumes: + - name: init + configMap: + name: init volumeClaimTemplates: - metadata: name: postgresql-db-disk @@ -46,3 +52,10 @@ spec: targetPort: 5432 selector: app: postgresql-db +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: init +data: + max_conns.sql: "ALTER SYSTEM SET max_connections = 500;" diff --git a/e2e/manifests/datastores/spanner.yaml b/e2e/databases/manifests/spanner.yaml similarity index 100% rename from e2e/manifests/datastores/spanner.yaml rename to e2e/databases/manifests/spanner.yaml diff --git a/e2e/databases/mysql.go b/e2e/databases/mysql.go new file mode 100644 index 00000000..ad7b008a --- /dev/null +++ b/e2e/databases/mysql.go @@ -0,0 +1,130 @@ +package databases + +import ( + "context" + "database/sql" + "fmt" + "path/filepath" + "time" + + "github.com/go-logr/logr" + sqlDriver "github.com/go-sql-driver/mysql" + "github.com/nightlyone/lockfile" + + //revive:disable:dot-imports convention is dot-import + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/storage/names" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + e2eutil "github.com/authzed/spicedb-operator/e2e/util" +) + +const MySQLAdminConnString = "root:password@tcp(localhost:%d)/mysql?parseTime=true" + +type MySQLProvider struct { + kclient kubernetes.Interface + namespace string + lock lockfile.Lockfile + mapper meta.RESTMapper + restConfig *rest.Config +} + +func NewMySQLProvider(mapper meta.RESTMapper, restConfig *rest.Config, namespace string) *MySQLProvider { + k, err := kubernetes.NewForConfig(restConfig) + Expect(err).To(Succeed()) + lockPath, err := filepath.Abs("./msql.lck") + Expect(err).To(Succeed()) + lock, err := lockfile.New(lockPath) + Expect(err).To(Succeed()) + return &MySQLProvider{kclient: k, namespace: namespace, lock: lock, mapper: mapper, restConfig: restConfig} +} + +func (p *MySQLProvider) New(ctx context.Context) *LogicalDatabase { + p.ensureDatabase(ctx) + var logicalDBName string + Eventually(func(g Gomega) { + logicalDBName = names.SimpleNameGenerator.GenerateName("my") + p.exec(ctx, g, fmt.Sprintf("CREATE DATABASE %s;", logicalDBName)) + }).Should(Succeed()) + return &LogicalDatabase{ + DatastoreURI: fmt.Sprintf("root:password@tcp(mysql-public.%s:3306)/%s?parseTime=true", p.namespace, logicalDBName), + DatabaseName: logicalDBName, + Engine: "mysql", + } +} + +func (p *MySQLProvider) Cleanup(ctx context.Context, db *LogicalDatabase) { + p.exec(ctx, Default, fmt.Sprintf("DROP DATABASE %s;", db.DatabaseName)) +} + +func (p *MySQLProvider) exec(ctx context.Context, g Gomega, cmd string) { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + ports := e2eutil.PortForward(g, p.namespace, "mysql-0", []string{":3306"}, ctx.Done()) + g.Expect(len(ports)).To(Equal(1)) + + dbConfig, err := sqlDriver.ParseDSN(fmt.Sprintf(MySQLAdminConnString, ports[0].Local)) + if err != nil { + GinkgoWriter.Println(err) + } + g.Expect(err).To(Succeed()) + + db, err := sql.Open("mysql", dbConfig.FormatDSN()) + if err != nil { + GinkgoWriter.Println(err) + } + g.Expect(err).To(Succeed()) + + _, err = db.ExecContext(ctx, cmd) + if err != nil { + GinkgoWriter.Println(err) + } + g.Expect(err).To(Succeed()) +} + +// ensureDatabase checks to see if the mysql instance has been set up, +// and if not, creates it under a lock +func (p *MySQLProvider) ensureDatabase(ctx context.Context) { + logger := logr.FromContextOrDiscard(ctx) + + for { + if p.running(ctx) == nil { + logger.V(4).Info("mysql found running") + return + } + logger.V(2).Info("mysql not running, attempting to acquire lock") + if err := p.lock.TryLock(); err != nil { + logger.V(2).Info("cannot acquire lock, retry") + continue + } + defer func() { + Expect(p.lock.Unlock()).To(Succeed()) + logger.V(4).Info("mysql lock released, creating database") + }() + logger.V(2).Info("mysql lock acquired, creating database") + + CreateFromManifests(ctx, p.namespace, "mysql", p.restConfig, p.mapper) + + Eventually(func(g Gomega) { + g.Expect(p.running(ctx)).To(Succeed()) + }).Should(Succeed()) + return + } +} + +func (p *MySQLProvider) running(ctx context.Context) error { + if _, err := p.kclient.CoreV1().Namespaces().Get(ctx, p.namespace, metav1.GetOptions{}); err != nil { + return err + } + + if _, err := p.kclient.AppsV1().StatefulSets(p.namespace).Get(ctx, "mysql", metav1.GetOptions{}); err != nil { + return err + } + + return nil +} diff --git a/e2e/databases/postgres.go b/e2e/databases/postgres.go new file mode 100644 index 00000000..f92734ed --- /dev/null +++ b/e2e/databases/postgres.go @@ -0,0 +1,125 @@ +package databases + +import ( + "context" + "fmt" + "path/filepath" + "time" + + "github.com/go-logr/logr" + "github.com/jackc/pgx/v5" + "github.com/nightlyone/lockfile" + + //revive:disable:dot-imports convention is dot-import + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/storage/names" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + e2eutil "github.com/authzed/spicedb-operator/e2e/util" +) + +const PGAdminConnString = "postgresql://postgres:testpassword@localhost:%d/postgres?sslmode=disable" + +type PostgresProvider struct { + kclient kubernetes.Interface + namespace string + lock lockfile.Lockfile + mapper meta.RESTMapper + restConfig *rest.Config +} + +func NewPostgresProvider(mapper meta.RESTMapper, restConfig *rest.Config, namespace string) *PostgresProvider { + k, err := kubernetes.NewForConfig(restConfig) + Expect(err).To(Succeed()) + lockPath, err := filepath.Abs("./postgres.lck") + Expect(err).To(Succeed()) + lock, err := lockfile.New(lockPath) + Expect(err).To(Succeed()) + return &PostgresProvider{kclient: k, namespace: namespace, lock: lock, mapper: mapper, restConfig: restConfig} +} + +func (p *PostgresProvider) New(ctx context.Context) *LogicalDatabase { + p.ensureDatabase(ctx) + var logicalDBName string + Eventually(func(g Gomega) { + logicalDBName = names.SimpleNameGenerator.GenerateName("pg") + p.exec(ctx, g, fmt.Sprintf("CREATE DATABASE %s;", logicalDBName)) + }).Should(Succeed()) + return &LogicalDatabase{ + DatastoreURI: fmt.Sprintf("postgresql://postgres:testpassword@postgresql-db-public.%s:5432/%s?sslmode=disable", p.namespace, logicalDBName), + DatabaseName: logicalDBName, + Engine: "postgres", + } +} + +func (p *PostgresProvider) Cleanup(ctx context.Context, db *LogicalDatabase) { + p.exec(ctx, Default, fmt.Sprintf("DROP DATABASE %s;", db.DatabaseName)) +} + +func (p *PostgresProvider) exec(ctx context.Context, g Gomega, cmd string) { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + ports := e2eutil.PortForward(g, p.namespace, "postgresql-db-0", []string{":5432"}, ctx.Done()) + g.Expect(len(ports)).To(Equal(1)) + conn, err := pgx.Connect(ctx, fmt.Sprintf(PGAdminConnString, ports[0].Local)) + if err != nil { + GinkgoWriter.Println(err) + } + + g.Expect(err).To(Succeed()) + _, err = conn.Exec(ctx, cmd) + if err != nil { + GinkgoWriter.Println(err) + } + g.Expect(err).To(Succeed()) +} + +// ensureDatabase checks to see if the postgres instance has been set up, +// and if not, creates it under a lock +func (p *PostgresProvider) ensureDatabase(ctx context.Context) { + logger := logr.FromContextOrDiscard(ctx) + + for { + if p.running(ctx) == nil { + logger.V(4).Info("pg found running") + return + } + logger.V(2).Info("pg not running, attempting to acquire lock") + if err := p.lock.TryLock(); err != nil { + logger.V(2).Info("cannot acquire lock, retry") + continue + } + defer func() { + Expect(p.lock.Unlock()).To(Succeed()) + logger.V(4).Info("pg lock released, creating database") + }() + logger.V(2).Info("pg lock acquired, creating database") + + CreateFromManifests(ctx, p.namespace, "postgres", p.restConfig, p.mapper) + + Eventually(func(g Gomega) { + g.Expect(p.running(ctx)).To(Succeed()) + }).Should(Succeed()) + Eventually(func(g Gomega) { + p.exec(ctx, g, "ALTER ROLE postgres CONNECTION LIMIT -1;") + }).Should(Succeed()) + return + } +} + +func (p *PostgresProvider) running(ctx context.Context) error { + if _, err := p.kclient.CoreV1().Namespaces().Get(ctx, p.namespace, metav1.GetOptions{}); err != nil { + return err + } + + if _, err := p.kclient.AppsV1().StatefulSets(p.namespace).Get(ctx, "postgresql-db", metav1.GetOptions{}); err != nil { + return err + } + + return nil +} diff --git a/e2e/databases/spanner.go b/e2e/databases/spanner.go new file mode 100644 index 00000000..bd96c075 --- /dev/null +++ b/e2e/databases/spanner.go @@ -0,0 +1,184 @@ +package databases + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + database "cloud.google.com/go/spanner/admin/database/apiv1" + instances "cloud.google.com/go/spanner/admin/instance/apiv1" + "github.com/go-logr/logr" + "github.com/nightlyone/lockfile" + + //revive:disable:dot-imports convention is dot-import + . "github.com/onsi/gomega" + "google.golang.org/api/option" + adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1" + "google.golang.org/genproto/googleapis/spanner/admin/instance/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/storage/names" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + e2eutil "github.com/authzed/spicedb-operator/e2e/util" +) + +type SpannerProvider struct { + kclient kubernetes.Interface + namespace string + lock lockfile.Lockfile + mapper meta.RESTMapper + restConfig *rest.Config +} + +func NewSpannerProvider(mapper meta.RESTMapper, restConfig *rest.Config, namespace string) *SpannerProvider { + k, err := kubernetes.NewForConfig(restConfig) + Expect(err).To(Succeed()) + lockPath, err := filepath.Abs("./spanner.lck") + Expect(err).To(Succeed()) + lock, err := lockfile.New(lockPath) + Expect(err).To(Succeed()) + return &SpannerProvider{kclient: k, namespace: namespace, lock: lock, mapper: mapper, restConfig: restConfig} +} + +func (p *SpannerProvider) New(ctx context.Context) *LogicalDatabase { + p.ensureDatabase(ctx) + var logicalDBName string + + Eventually(func(g Gomega) { + logicalDBName = names.SimpleNameGenerator.GenerateName("sp") + p.execDatabase(ctx, g, func(client *database.DatabaseAdminClient) { + op, err := client.CreateDatabase(ctx, &adminpb.CreateDatabaseRequest{ + Parent: "projects/fake-project-id/instances/fake-instance", + CreateStatement: "CREATE DATABASE `" + logicalDBName + "`", + }) + g.Expect(err).To(Succeed()) + + _, err = op.Wait(ctx) + g.Expect(err).To(Succeed()) + }) + }).Should(Succeed()) + return &LogicalDatabase{ + DatastoreURI: fmt.Sprintf("projects/fake-project-id/instances/fake-instance/databases/%s", logicalDBName), + DatabaseName: logicalDBName, + ExtraConfig: map[string]string{ + "datastoreSpannerEmulatorHost": fmt.Sprintf("spanner-service.%s:9010", p.namespace), + }, + Engine: "spanner", + } +} + +func (p *SpannerProvider) Cleanup(ctx context.Context, db *LogicalDatabase) { + // TODO: figure out how to cleanup a spanner emulator db +} + +func (p *SpannerProvider) execInstance(ctx context.Context, g Gomega, cmd func(client *instances.InstanceAdminClient)) { + ctx, cancel := context.WithTimeout(ctx, 500*time.Second) + defer cancel() + ports := e2eutil.PortForward(Default, p.namespace, "spanner-0", []string{":9010"}, ctx.Done()) + g.Expect(len(ports)).To(Equal(1)) + + Expect(os.Setenv("SPANNER_EMULATOR_HOST", fmt.Sprintf("localhost:%d", ports[0].Local))).To(Succeed()) + + var instancesClient *instances.InstanceAdminClient + g.Eventually(func() error { + var err error + instancesClient, err = instances.NewInstanceAdminClient(ctx, + option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), + option.WithoutAuthentication(), + option.WithEndpoint(fmt.Sprintf("localhost:%d", ports[0].Local))) + return err + }).Should(Succeed()) + defer func() { Expect(instancesClient.Close()).To(Succeed()) }() + + cmd(instancesClient) +} + +func (p *SpannerProvider) execDatabase(ctx context.Context, g Gomega, cmd func(client *database.DatabaseAdminClient)) { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + ports := e2eutil.PortForward(Default, p.namespace, "spanner-0", []string{":9010"}, ctx.Done()) + g.Expect(len(ports)).To(Equal(1)) + + adminClient, err := database.NewDatabaseAdminClient(ctx, + option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())), + option.WithoutAuthentication(), + option.WithEndpoint(fmt.Sprintf("localhost:%d", ports[0].Local))) + Expect(err).To(Succeed()) + defer func() { + Expect(adminClient.Close()).To(Succeed()) + }() + + cmd(adminClient) +} + +// ensureDatabase checks to see if the spanner instance has been set up, +// and if not, creates it under a lock +func (p *SpannerProvider) ensureDatabase(ctx context.Context) { + logger := logr.FromContextOrDiscard(ctx) + + for { + if p.running(ctx) == nil { + logger.V(4).Info("spanner found running") + return + } + logger.V(2).Info("spanner not running, attempting to acquire lock") + if err := p.lock.TryLock(); err != nil { + logger.V(2).Info("cannot acquire lock, retry") + continue + } + defer func() { + Expect(p.lock.Unlock()).To(Succeed()) + logger.V(4).Info("spanner lock released, creating database") + }() + logger.V(2).Info("spanner lock acquired, creating database") + + CreateFromManifests(ctx, p.namespace, "spanner", p.restConfig, p.mapper) + + Eventually(func(g Gomega) { + p.execInstance(ctx, g, func(client *instances.InstanceAdminClient) { + createInstanceOp, err := client.CreateInstance(ctx, &instance.CreateInstanceRequest{ + Parent: "projects/fake-project-id", + InstanceId: "fake-instance", + Instance: &instance.Instance{ + Config: "emulator-config", + DisplayName: "Test Instance", + NodeCount: 1, + }, + }) + g.Expect(err).To(Succeed()) + _, err = createInstanceOp.Wait(ctx) + g.Expect(err).To(Succeed()) + }) + }).Should(Succeed()) + Eventually(func(g Gomega) { + g.Expect(p.running(ctx)).To(Succeed()) + }) + return + } +} + +func (p *SpannerProvider) running(ctx context.Context) error { + if _, err := p.kclient.CoreV1().Namespaces().Get(ctx, p.namespace, metav1.GetOptions{}); err != nil { + return err + } + + if _, err := p.kclient.AppsV1().StatefulSets(p.namespace).Get(ctx, "spanner", metav1.GetOptions{}); err != nil { + return err + } + + var err error + p.execInstance(ctx, Default, func(client *instances.InstanceAdminClient) { + var i *instance.Instance + i, err = client.GetInstance(ctx, &instance.GetInstanceRequest{ + Name: "projects/fake-project-id/instances/fake-instance", + }) + fmt.Println(i) + }) + return err +} diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 434807fb..500b092f 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -18,8 +18,7 @@ import ( "time" "github.com/go-logr/zapr" - utilyaml "k8s.io/apimachinery/pkg/util/yaml" - + "github.com/jzelinskie/stringz" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" @@ -29,12 +28,16 @@ import ( v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + utilyaml "k8s.io/apimachinery/pkg/util/yaml" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/cli-runtime/pkg/genericclioptions" applyv1 "k8s.io/client-go/applyconfigurations/core/v1" + "k8s.io/client-go/discovery" + "k8s.io/client-go/discovery/cached/memory" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "k8s.io/client-go/restmapper" "k8s.io/klog/v2" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/utils/pointer" @@ -51,6 +54,8 @@ import ( "sigs.k8s.io/kind/pkg/fs" "sigs.k8s.io/yaml" + "github.com/authzed/spicedb-operator/e2e/databases" + e2eutil "github.com/authzed/spicedb-operator/e2e/util" "github.com/authzed/spicedb-operator/pkg/cmd/run" "github.com/authzed/spicedb-operator/pkg/config" ) @@ -64,12 +69,16 @@ var ( // - if run with `APISERVER_ONLY=true`, we'll use apiserver + etcd instead of a real cluster // - if run with `PROVISION=false` and `APISERVER_ONLY=false` we'll use the environment / kubeconfig flags to connect to an existing cluster - apiserverOnly = os.Getenv("APISERVER_ONLY") == "true" - provision = os.Getenv("PROVISION") == "true" - archives = strings.FieldsFunc(os.Getenv("ARCHIVES"), listsep) - images = strings.FieldsFunc(os.Getenv("IMAGES"), listsep) + apiserverOnly = os.Getenv("APISERVER_ONLY") == "true" + provision = os.Getenv("PROVISION") == "true" + archives = strings.FieldsFunc(os.Getenv("ARCHIVES"), listsep) + images = strings.FieldsFunc(os.Getenv("IMAGES"), listsep) + ProposedGraphFile = stringz.DefaultEmpty(os.Getenv("PROPOSED_GRAPH_FILE"), "../proposed-update-graph.yaml") + ValidatedGraphFile = stringz.DefaultEmpty(os.Getenv("VALIDATED_GRAPH_FILE"), "../validated-update-graph.yaml") restConfig *rest.Config + + postgresProvider, mysqlProvider, crdbProvider databases.Provider ) func init() { @@ -114,6 +123,14 @@ var _ = SynchronizedBeforeSuite(func() []byte { run.DisableClientRateLimits(config) + restConfig = config + e2eutil.RestConfig = config + + seed := GinkgoRandomSeed() + CreateNamespace(fmt.Sprintf("postgres-%d", seed)) + CreateNamespace(fmt.Sprintf("mysql-%d", seed)) + CreateNamespace(fmt.Sprintf("cockroachdb-%d", seed)) + StartOperator(config) var buf bytes.Buffer enc := gob.NewEncoder(&buf) @@ -125,6 +142,26 @@ var _ = SynchronizedBeforeSuite(func() []byte { var config rest.Config Expect(dec.Decode(&config)).To(Succeed()) restConfig = &config + e2eutil.RestConfig = &config + dc, err := discovery.NewDiscoveryClientForConfig(restConfig) + Expect(err).To(Succeed()) + mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(dc)) + + // Databases (except spanner) are spun up once per test suite run, + // with parallel tests using different logical databases. + // The kube resources are created lazily and won't deploy if no tests in + // focus need that database. + seed := GinkgoRandomSeed() + postgresProvider = databases.NewPostgresProvider(mapper, restConfig, fmt.Sprintf("postgres-%d", seed)) + mysqlProvider = databases.NewMySQLProvider(mapper, restConfig, fmt.Sprintf("mysql-%d", seed)) + crdbProvider = databases.NewCockroachProvider(mapper, restConfig, fmt.Sprintf("cockroachdb-%d", seed)) +}) + +var _ = SynchronizedAfterSuite(func() {}, func() { + seed := GinkgoRandomSeed() + DeleteNamespace(fmt.Sprintf("postgres-%d", seed)) + DeleteNamespace(fmt.Sprintf("mysql-%d", seed)) + DeleteNamespace(fmt.Sprintf("cockroachdb-%d", seed)) }) func CreateNamespace(name string) { @@ -154,7 +191,7 @@ func StartOperator(rc *rest.Config) { options := run.RecommendedOptions() options.DebugAddress = ":" options.BootstrapCRDs = true - options.OperatorConfigPath = WriteConfig(GetDefaultConfig()) + options.OperatorConfigPath = WriteConfig(GetConfig(ProposedGraphFile)) _ = options.Run(ctx, cmdutil.NewFactory(ClientGetter{config: rc})) }() @@ -190,12 +227,19 @@ func WriteConfig(operatorConfig config.OperatorConfig) string { return ConfigFileName } -func GetDefaultConfig() (cfg config.OperatorConfig) { - file, err := os.Open("../default-operator-config.yaml") +func GetConfig(fileName string) (cfg config.OperatorConfig) { + file, err := os.Open(fileName) + if err != nil { + fmt.Println(err) + } Expect(err).To(Succeed()) defer file.Close() decoder := utilyaml.NewYAMLOrJSONDecoder(file, 100) - Expect(decoder.Decode(&cfg)).To(Succeed()) + err = decoder.Decode(&cfg) + if err != nil { + fmt.Println(err) + } + Expect(err).To(Succeed()) return } diff --git a/e2e/util/port_forward.go b/e2e/util/port_forward.go new file mode 100644 index 00000000..176d2e78 --- /dev/null +++ b/e2e/util/port_forward.go @@ -0,0 +1,74 @@ +package util + +import ( + "net/http" + + //revive:disable:dot-imports convention is dot-import + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/transport/spdy" +) + +var RestConfig *rest.Config + +func PortForward(g Gomega, namespace, podName string, ports []string, stopChan <-chan struct{}) []portforward.ForwardedPort { + GinkgoHelper() + + restConfig := rest.CopyConfig(RestConfig) + transport, upgrader, err := spdy.RoundTripperFor(restConfig) + if err != nil { + GinkgoWriter.Println(err) + } + g.Expect(err).To(Succeed()) + readyc := make(chan struct{}) + restConfig.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"} + if restConfig.APIPath == "" { + restConfig.APIPath = "/api" + } + if restConfig.NegotiatedSerializer == nil { + restConfig.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + } + g.Expect(rest.SetKubernetesDefaults(restConfig)).To(Succeed()) + restClient, err := rest.RESTClientFor(restConfig) + g.Expect(err).To(Succeed()) + + req := restClient.Post(). + Resource("pods"). + Namespace(namespace). + Name(podName). + SubResource("portforward") + + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL()) + fw, err := portforward.New(dialer, ports, stopChan, readyc, GinkgoWriter, GinkgoWriter) + if err != nil { + GinkgoWriter.Println(err) + } + g.Expect(err).To(Succeed()) + go func() { + defer GinkgoRecover() + err := fw.ForwardPorts() + if err != nil { + GinkgoWriter.Println(err) + } + + g.Expect(err).To(Succeed()) + }() + for { + select { + case <-stopChan: + GinkgoWriter.Println("port-forward canceled") + return nil + case <-readyc: + localports, err := fw.GetPorts() + if err != nil { + GinkgoWriter.Println(err) + } + g.Expect(err).To(Succeed()) + return localports + } + } +} diff --git a/e2e/util_test.go b/e2e/util_test.go index 207b10d6..d93314c3 100644 --- a/e2e/util_test.go +++ b/e2e/util_test.go @@ -4,16 +4,13 @@ package e2e import ( "context" - "embed" "encoding/pem" "fmt" "io" - "net/http" "os" "path/filepath" "time" - "github.com/fluxcd/pkg/ssa" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" @@ -23,56 +20,19 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/discovery" "k8s.io/client-go/discovery/cached/disk" "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/tools/portforward" - "k8s.io/client-go/transport/spdy" "k8s.io/client-go/util/cert" "k8s.io/kubectl/pkg/cmd/logs" "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling" - crClient "sigs.k8s.io/controller-runtime/pkg/client" ) -func PortForward(namespace, podName string, ports []string, stopChan <-chan struct{}) { - transport, upgrader, err := spdy.RoundTripperFor(restConfig) - Expect(err).To(Succeed()) - readyc := make(chan struct{}) - restConfig.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"} - if restConfig.APIPath == "" { - restConfig.APIPath = "/api" - } - if restConfig.NegotiatedSerializer == nil { - restConfig.NegotiatedSerializer = scheme.Codecs.WithoutConversion() - } - Expect(rest.SetKubernetesDefaults(restConfig)).To(Succeed()) - restClient, err := rest.RESTClientFor(restConfig) - Expect(err).To(Succeed()) - - req := restClient.Post(). - Resource("pods"). - Namespace(namespace). - Name(podName). - SubResource("portforward") - - dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL()) - fw, err := portforward.New(dialer, ports, stopChan, readyc, GinkgoWriter, GinkgoWriter) - Expect(err).To(Succeed()) - go func() { - defer GinkgoRecover() - Expect(fw.ForwardPorts()).To(Succeed()) - }() - <-readyc -} - // TailF adds the logs from the object to the Ginkgo output func TailF(obj runtime.Object) { Tail(obj, func(Gomega) {}, GinkgoWriter) @@ -83,25 +43,25 @@ func TailF(obj runtime.Object) { func Tail(obj runtime.Object, assert func(g Gomega), writers ...io.Writer) { go func() { defer GinkgoRecover() - logger := logs.NewLogsOptions(genericclioptions.IOStreams{ - In: os.Stdin, - Out: io.MultiWriter(writers...), - ErrOut: io.MultiWriter(writers...), - }, true) - logger.Follow = false - logger.IgnoreLogErrors = false - logger.Object = obj - logger.RESTClientGetter = util.NewFactory(ClientGetter{config: restConfig}) - logger.LogsForObject = polymorphichelpers.LogsForObjectFn - logger.ConsumeRequestFn = logs.DefaultConsumeRequest - var err error - logger.Options, err = logger.ToLogOptions() - Expect(err).To(Succeed()) - Expect(logger.Validate()).To(Succeed()) Eventually(func(g Gomega) { + logger := logs.NewLogsOptions(genericclioptions.IOStreams{ + In: os.Stdin, + Out: io.MultiWriter(writers...), + ErrOut: io.MultiWriter(writers...), + }, true) + logger.Follow = false + logger.IgnoreLogErrors = false + logger.Object = obj + logger.RESTClientGetter = util.NewFactory(ClientGetter{config: restConfig}) + logger.LogsForObject = polymorphichelpers.LogsForObjectFn + logger.ConsumeRequestFn = logs.DefaultConsumeRequest + var err error + logger.Options, err = logger.ToLogOptions() + g.Expect(err).To(Succeed()) + g.Expect(logger.Validate()).To(Succeed()) g.Expect(logger.RunLogs()).To(Succeed()) assert(g) - }).WithTimeout(4 * time.Minute).WithPolling(2 * time.Second).Should(Succeed()) + }).Should(Succeed()) }() } @@ -151,48 +111,6 @@ func GenerateCertManagerCompliantTLSSecretForService(service, secret types.Names } } -//go:embed manifests/datastores/*.yaml -var datastores embed.FS - -// CreateDatabase reads a database yaml definition from a file and creates it -func CreateDatabase(ctx context.Context, mapper meta.RESTMapper, namespace string, engine string) { - ssaClient, err := crClient.NewWithWatch(restConfig, crClient.Options{ - Mapper: mapper, - }) - Expect(err).To(Succeed()) - resourceManager := ssa.NewResourceManager(ssaClient, polling.NewStatusPoller(ssaClient, mapper, polling.Options{}), ssa.Owner{ - Field: "test.authzed.com", - Group: "test.authzed.com", - }) - - yamlReader, err := datastores.Open(fmt.Sprintf("manifests/datastores/%s.yaml", engine)) - Expect(err).To(Succeed()) - DeferCleanup(yamlReader.Close) - - decoder := yaml.NewYAMLToJSONDecoder(yamlReader) - objs := make([]*unstructured.Unstructured, 0) - for { - u := &unstructured.Unstructured{Object: map[string]interface{}{}} - err := decoder.Decode(&u.Object) - if err == io.EOF { - break - } else { - Expect(err).To(Succeed()) - } - u.SetNamespace(namespace) - objs = append(objs, u) - } - _, err = resourceManager.ApplyAll(ctx, objs, ssa.DefaultApplyOptions()) - Expect(err).To(Succeed()) - By(fmt.Sprintf("waiting for %s to start..", engine)) - err = resourceManager.Wait(objs, ssa.WaitOptions{ - Interval: 1 * time.Second, - Timeout: 90 * time.Second, - }) - Expect(err).To(Succeed()) - By(fmt.Sprintf("%s running", engine)) -} - // ClientGetter implements RESTClientGetter to return values for the configured // test. type ClientGetter struct { diff --git a/go.mod b/go.mod index 6f028e05..485a8b74 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/authzed/spicedb-operator -go 1.20 +go 1.19 require ( cloud.google.com/go/spanner v1.39.0 @@ -10,10 +10,12 @@ require ( github.com/fluxcd/pkg/ssa v0.23.0 github.com/go-logr/logr v1.2.3 github.com/go-logr/zapr v1.2.3 + github.com/go-sql-driver/mysql v1.7.0 github.com/jackc/pgx/v5 v5.2.0 github.com/jzelinskie/stringz v0.0.1 github.com/magefile/mage v1.14.0 github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0 + github.com/nightlyone/lockfile v1.0.0 github.com/onsi/ginkgo/v2 v2.8.0 github.com/onsi/gomega v1.25.0 github.com/spf13/afero v1.9.2 @@ -22,7 +24,9 @@ require ( go.uber.org/atomic v1.10.0 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20220823124025-807a23277127 + google.golang.org/api v0.96.0 google.golang.org/genproto v0.0.0-20220916134934-764224ccc2d1 + google.golang.org/grpc v1.49.0 k8s.io/api v0.26.1 k8s.io/apiextensions-apiserver v0.26.0 k8s.io/apimachinery v0.26.1 @@ -150,9 +154,7 @@ require ( golang.org/x/text v0.6.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.5.0 // indirect - google.golang.org/api v0.96.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/grpc v1.49.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 2671ceda..269d8fb9 100644 --- a/go.sum +++ b/go.sum @@ -196,6 +196,8 @@ github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXym github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -403,6 +405,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= +github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.8.0 h1:pAM+oBNPrpXRs+E/8spkeGx9QgekbRVyr74EUvRVOUI= diff --git a/goreleaser.yaml b/goreleaser.yaml index 6897341a..dcc435e7 100644 --- a/goreleaser.yaml +++ b/goreleaser.yaml @@ -27,7 +27,7 @@ dockers: build_flag_templates: - "--platform=linux/amd64" extra_files: - - "default-operator-config.yaml" + - "validated-update-graph.yaml" # ARM64 - image_templates: - &arm_image_quay "quay.io/authzed/spicedb-operator:v{{ .Version }}-arm64" @@ -40,7 +40,7 @@ dockers: build_flag_templates: - "--platform=linux/arm64" extra_files: - - "default-operator-config.yaml" + - "validated-update-graph.yaml" docker_manifests: # Quay - name_template: "quay.io/authzed/spicedb-operator:v{{ .Version }}" diff --git a/magefiles/magefile.go b/magefiles/magefile.go index b42d180f..85720cea 100644 --- a/magefiles/magefile.go +++ b/magefiles/magefile.go @@ -5,12 +5,17 @@ package main import ( "fmt" + "io" + "os" "os/exec" + "github.com/cespare/xxhash/v2" "github.com/magefile/mage/mg" "github.com/magefile/mage/sh" + "github.com/magefile/mage/target" kind "sigs.k8s.io/kind/pkg/cluster" "sigs.k8s.io/kind/pkg/cmd" + "sigs.k8s.io/kind/pkg/fs" ) var Aliases = map[string]interface{}{ @@ -24,25 +29,52 @@ type Test mg.Namespace // Runs the unit tests func (Test) Unit() error { fmt.Println("running unit tests") - goCmd := "go" - if hasBinary("richgo") { - goCmd = "richgo" - } - return sh.RunV(goCmd, "test", "./...") + return sh.RunV(goCmdForTests(), "test", "./...") } // Runs the end-to-end tests in a kind cluster func (Test) E2e() error { - mg.Deps(checkDocker) + mg.Deps(checkDocker, Gen{}.generateGraphIfSourcesChanged) fmt.Println("running e2e tests") - return sh.RunWithV(map[string]string{ - "PROVISION": "true", - }, "go", "run", "github.com/onsi/ginkgo/v2/ginkgo", "--tags=e2e", "-p", "-r", "-v", "--fail-fast", "--randomize-all", "--race", "e2e") + + if err := sh.RunWithV(map[string]string{ + "PROVISION": "true", + "SPICEDB_CMD": os.Getenv("SPICEDB_CMD"), + "SPICEDB_ENV_PREFIX": os.Getenv("SPICEDB_ENV_PREFIX"), + "ARCHIVES": os.Getenv("ARCHIVES"), + "IMAGES": os.Getenv("IMAGES"), + "PROPOSED_GRAPH_FILE": os.Getenv("PROPOSED_GRAPH_FILE"), + "VALIDATED_GRAPH_FILE": os.Getenv("VALIDATED_GRAPH_FILE"), + }, "go", "run", "github.com/onsi/ginkgo/v2/ginkgo", "--tags=e2e", "-p", "-r", "-vv", "--fail-fast", "--randomize-all", "e2e"); err != nil { + return err + } + + const ( + ProposedGraphFile = "proposed-update-graph.yaml" + ValidatedGraphFile = "validated-update-graph.yaml" + ) + + equal, err := fileEqual(ProposedGraphFile, ValidatedGraphFile) + if err != nil { + return err + } + + if !equal { + fmt.Println("marking update graph as validated after successful test run") + return fs.CopyFile("proposed-update-graph.yaml", "validated-update-graph.yaml") + } + fmt.Println("no changes to update graph") + + return nil } // Removes the kind cluster used for end-to-end tests func (Test) Clean_e2e() error { mg.Deps(checkDocker) + fmt.Println("removing saved cluster state") + if err := os.RemoveAll("./e2e/cluster-state"); err != nil { + return err + } fmt.Println("removing kind cluster") return kind.NewProvider( kind.ProviderWithLogger(cmd.NewLogger()), @@ -69,6 +101,18 @@ func (Gen) Graph() error { return sh.RunV("go", "generate", "./tools/generate-update-graph/main.go") } +// If the update graph definition +func (g Gen) generateGraphIfSourcesChanged() error { + regen, err := target.Dir("proposed-update-graph.yaml", "tools/generate-update-graph") + if err != nil { + return err + } + if regen { + return g.Graph() + } + return nil +} + func checkDocker() error { if !hasBinary("docker") { return fmt.Errorf("docker must be installed to run e2e tests") @@ -84,3 +128,33 @@ func hasBinary(binaryName string) bool { _, err := exec.LookPath(binaryName) return err == nil } + +func goCmdForTests() string { + if hasBinary("richgo") { + return "richgo" + } + return "go" +} + +func fileEqual(a, b string) (bool, error) { + aFile, err := os.Open(a) + if err != nil { + return false, err + } + aHash := xxhash.New() + _, err = io.Copy(aHash, aFile) + if err != nil { + return false, err + } + bFile, err := os.Open(b) + if err != nil { + return false, err + } + bHash := xxhash.New() + _, err = io.Copy(bHash, bFile) + if err != nil { + return false, err + } + + return aHash.Sum64() == bHash.Sum64(), nil +} diff --git a/pkg/crds/authzed.com_spicedbclusters.yaml b/pkg/crds/authzed.com_spicedbclusters.yaml index 33e5e929..66bb951f 100644 --- a/pkg/crds/authzed.com_spicedbclusters.yaml +++ b/pkg/crds/authzed.com_spicedbclusters.yaml @@ -14,10 +14,40 @@ spec: kind: SpiceDBCluster listKind: SpiceDBClusterList plural: spicedbclusters + shortNames: + - spicedbs singular: spicedbcluster scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .spec.channel + name: Channel + type: string + - jsonPath: .spec.version + name: Desired + type: string + - jsonPath: .status.version.name + name: Current + type: string + - jsonPath: .status.conditions[?(@.type=='ConfigurationWarning')].status + name: Warnings + type: string + - jsonPath: .status.conditions[?(@.type=='Migrating')].status + name: Migrating + type: string + - jsonPath: .status.conditions[?(@.type=='RollingDeployment')].status + name: Updating + type: string + - jsonPath: .status.conditions[?(@.type=='ConditionValidatingFailed')].status + name: Invalid + type: string + - jsonPath: .status.conditions[?(@.type=='Paused')].status + name: Paused + type: string + name: v1alpha1 schema: openAPIV3Schema: description: SpiceDBCluster defines all options for a full SpiceDB cluster diff --git a/pkg/updates/file.go b/pkg/updates/file.go index e6e7dcb3..a40d1991 100644 --- a/pkg/updates/file.go +++ b/pkg/updates/file.go @@ -295,7 +295,6 @@ func (g *UpdateGraph) Difference(other *UpdateGraph) *UpdateGraph { // Find matching channels between the graphs for _, thisChannel := range g.Channels { - foundMatchingChannel := false for _, otherChannel := range other.Channels { diff --git a/default-operator-config.yaml b/proposed-update-graph.yaml similarity index 75% rename from default-operator-config.yaml rename to proposed-update-graph.yaml index 80bb4337..8fcaf276 100644 --- a/default-operator-config.yaml +++ b/proposed-update-graph.yaml @@ -1,34 +1,5 @@ channels: - edges: - v1.0.0: - - v1.1.0 - - v1.2.0 - - v1.3.0 - - v1.4.0 - - v1.5.0 - - v1.6.0 - - v1.7.1 - - v1.8.0 - - v1.9.0 - - v1.10.0 - - v1.11.0 - - v1.12.0 - - v1.13.0 - - v1.14.0-phase1 - v1.1.0: - - v1.2.0 - - v1.3.0 - - v1.4.0 - - v1.5.0 - - v1.6.0 - - v1.7.1 - - v1.8.0 - - v1.9.0 - - v1.10.0 - - v1.11.0 - - v1.12.0 - - v1.13.0 - - v1.14.0-phase1 v1.2.0: - v1.3.0 - v1.4.0 @@ -213,48 +184,7 @@ channels: - id: v1.2.0 migration: add-transaction-timestamp-index tag: v1.2.0 - - id: v1.1.0 - migration: add-transaction-timestamp-index - tag: v1.1.0 - - id: v1.0.0 - migration: add-unique-living-ns - tag: v1.0.0 - edges: - v1.0.0: - - v1.1.0 - - v1.2.0 - - v1.3.0 - - v1.4.0 - - v1.5.0 - - v1.6.0 - - v1.7.1 - - v1.8.0 - - v1.9.0 - - v1.10.0 - - v1.11.0 - - v1.12.0 - - v1.13.0 - - v1.14.1 - - v1.15.0 - - v1.16.0 - - v1.16.1 - v1.1.0: - - v1.2.0 - - v1.3.0 - - v1.4.0 - - v1.5.0 - - v1.6.0 - - v1.7.1 - - v1.8.0 - - v1.9.0 - - v1.10.0 - - v1.11.0 - - v1.12.0 - - v1.13.0 - - v1.14.1 - - v1.15.0 - - v1.16.0 - - v1.16.1 v1.2.0: - v1.3.0 - v1.4.0 @@ -466,12 +396,6 @@ channels: - id: v1.2.0 migration: add-transactions-table tag: v1.2.0 - - id: v1.1.0 - migration: add-transactions-table - tag: v1.1.0 - - id: v1.0.0 - migration: add-transactions-table - tag: v1.0.0 - edges: v1.7.0: - v1.7.1 @@ -600,134 +524,6 @@ channels: migration: add_unique_datastore_id tag: v1.7.0 - edges: - v1.0.0: - - v1.1.0 - - v1.2.0 - - v1.3.0 - - v1.4.0 - - v1.5.0 - - v1.6.0 - - v1.7.1 - - v1.8.0 - - v1.9.0 - - v1.10.0 - - v1.11.0 - - v1.12.0 - - v1.13.0 - - v1.14.1 - - v1.15.0 - - v1.16.0 - - v1.16.1 - v1.1.0: - - v1.2.0 - - v1.3.0 - - v1.4.0 - - v1.5.0 - - v1.6.0 - - v1.7.1 - - v1.8.0 - - v1.9.0 - - v1.10.0 - - v1.11.0 - - v1.12.0 - - v1.13.0 - - v1.14.1 - - v1.15.0 - - v1.16.0 - - v1.16.1 - v1.2.0: - - v1.3.0 - - v1.4.0 - - v1.5.0 - - v1.6.0 - - v1.7.1 - - v1.8.0 - - v1.9.0 - - v1.10.0 - - v1.11.0 - - v1.12.0 - - v1.13.0 - - v1.14.1 - - v1.15.0 - - v1.16.0 - - v1.16.1 - v1.3.0: - - v1.4.0 - - v1.5.0 - - v1.6.0 - - v1.7.1 - - v1.8.0 - - v1.9.0 - - v1.10.0 - - v1.11.0 - - v1.12.0 - - v1.13.0 - - v1.14.1 - - v1.15.0 - - v1.16.0 - - v1.16.1 - v1.4.0: - - v1.5.0 - - v1.6.0 - - v1.7.1 - - v1.8.0 - - v1.9.0 - - v1.10.0 - - v1.11.0 - - v1.12.0 - - v1.13.0 - - v1.14.1 - - v1.15.0 - - v1.16.0 - - v1.16.1 - v1.5.0: - - v1.6.0 - - v1.7.1 - - v1.8.0 - - v1.9.0 - - v1.10.0 - - v1.11.0 - - v1.12.0 - - v1.13.0 - - v1.14.1 - - v1.15.0 - - v1.16.0 - - v1.16.1 - v1.6.0: - - v1.7.1 - - v1.8.0 - - v1.9.0 - - v1.10.0 - - v1.11.0 - - v1.12.0 - - v1.13.0 - - v1.14.1 - - v1.15.0 - - v1.16.0 - - v1.16.1 - v1.7.0: - - v1.7.1 - - v1.8.0 - - v1.9.0 - - v1.10.0 - - v1.11.0 - - v1.12.0 - - v1.13.0 - - v1.14.1 - - v1.15.0 - - v1.16.0 - - v1.16.1 - v1.7.1: - - v1.8.0 - - v1.9.0 - - v1.10.0 - - v1.11.0 - - v1.12.0 - - v1.13.0 - - v1.14.1 - - v1.15.0 - - v1.16.0 - - v1.16.1 v1.8.0: - v1.9.0 - v1.10.0 @@ -825,31 +621,4 @@ channels: - id: v1.8.0 migration: add-metadata-and-counters tag: v1.8.0 - - id: v1.7.1 - migration: add-metadata-and-counters - tag: v1.7.1 - - id: v1.7.0 - migration: add-metadata-and-counters - tag: v1.7.0 - - id: v1.6.0 - migration: add-metadata-and-counters - tag: v1.6.0 - - id: v1.5.0 - migration: initial - tag: v1.5.0 - - id: v1.4.0 - migration: initial - tag: v1.4.0 - - id: v1.3.0 - migration: initial - tag: v1.3.0 - - id: v1.2.0 - migration: initial - tag: v1.2.0 - - id: v1.1.0 - migration: initial - tag: v1.1.0 - - id: v1.0.0 - migration: initial - tag: v1.0.0 imageName: ghcr.io/authzed/spicedb diff --git a/tools/generate-update-graph/main.go b/tools/generate-update-graph/main.go index 5ff38d6e..2bfe126d 100644 --- a/tools/generate-update-graph/main.go +++ b/tools/generate-update-graph/main.go @@ -1,38 +1,22 @@ package main import ( - "context" "fmt" "os" - "github.com/google/go-github/v43/github" "sigs.k8s.io/yaml" "github.com/authzed/spicedb-operator/pkg/config" "github.com/authzed/spicedb-operator/pkg/updates" ) -//go:generate go run main.go ../../default-operator-config.yaml - -const ( - githubNamespace = "authzed" - githubRepository = "spicedb" -) +//go:generate go run main.go ../../proposed-update-graph.yaml func main() { if len(os.Args) != 2 { fmt.Println("must provide filename") os.Exit(1) } - ctx := context.Background() - client := github.NewClient(nil) - - // this returns the newest release by date, not by version - // note that spicedb uses the same API to determine if it's up to date - latestRelease, _, err := client.Repositories.GetLatestRelease(ctx, githubNamespace, githubRepository) - if err != nil { - panic(err) - } opconfig := config.OperatorConfig{ ImageName: "ghcr.io/authzed/spicedb", @@ -44,14 +28,6 @@ func main() { }}, } - for _, c := range opconfig.Channels { - if c.Nodes[0].Tag != *latestRelease.Name { - fmt.Printf("channel %q does not contain the latest release %q\n", c.Name, *latestRelease.Name) - os.Exit(1) - return - } - } - yamlBytes, err := yaml.Marshal(&opconfig) if err != nil { panic(err) @@ -84,16 +60,12 @@ func postgresChannel() updates.Channel { {ID: "v1.4.0", Tag: "v1.4.0", Migration: "add-transaction-timestamp-index"}, {ID: "v1.3.0", Tag: "v1.3.0", Migration: "add-transaction-timestamp-index"}, {ID: "v1.2.0", Tag: "v1.2.0", Migration: "add-transaction-timestamp-index"}, - {ID: "v1.1.0", Tag: "v1.1.0", Migration: "add-transaction-timestamp-index"}, - {ID: "v1.0.0", Tag: "v1.0.0", Migration: "add-unique-living-ns"}, } return updates.Channel{ - ChannelID: updates.ChannelID{ - Name: "stable", - Metadata: map[string]string{ - "datastore": "postgres", - "default": "true", - }, + Name: "stable", + Metadata: map[string]string{ + "datastore": "postgres", + "default": "true", }, Nodes: releases, Edges: map[string][]string{ @@ -116,8 +88,6 @@ func postgresChannel() updates.Channel { "v1.4.0": {"v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.0-phase1"}, "v1.3.0": {"v1.4.0", "v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.0-phase1"}, "v1.2.0": {"v1.3.0", "v1.4.0", "v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.0-phase1"}, - "v1.1.0": {"v1.2.0", "v1.3.0", "v1.4.0", "v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.0-phase1"}, - "v1.0.0": {"v1.1.0", "v1.2.0", "v1.3.0", "v1.4.0", "v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.0-phase1"}, }, } } @@ -142,16 +112,12 @@ func crdbChannel() updates.Channel { {ID: "v1.4.0", Tag: "v1.4.0", Migration: "add-transactions-table"}, {ID: "v1.3.0", Tag: "v1.3.0", Migration: "add-transactions-table"}, {ID: "v1.2.0", Tag: "v1.2.0", Migration: "add-transactions-table"}, - {ID: "v1.1.0", Tag: "v1.1.0", Migration: "add-transactions-table"}, - {ID: "v1.0.0", Tag: "v1.0.0", Migration: "add-transactions-table"}, } return updates.Channel{ - ChannelID: updates.ChannelID{ - Name: "stable", - Metadata: map[string]string{ - "datastore": "cockroachdb", - "default": "true", - }, + Name: "stable", + Metadata: map[string]string{ + "datastore": "cockroachdb", + "default": "true", }, Nodes: releases, Edges: map[string][]string{ @@ -172,8 +138,6 @@ func crdbChannel() updates.Channel { "v1.4.0": {"v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, "v1.3.0": {"v1.4.0", "v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, "v1.2.0": {"v1.3.0", "v1.4.0", "v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, - "v1.1.0": {"v1.2.0", "v1.3.0", "v1.4.0", "v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, - "v1.0.0": {"v1.1.0", "v1.2.0", "v1.3.0", "v1.4.0", "v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, }, } } @@ -195,12 +159,10 @@ func mysqlChannel() updates.Channel { {ID: "v1.7.0", Tag: "v1.7.0", Migration: "add_unique_datastore_id"}, } return updates.Channel{ - ChannelID: updates.ChannelID{ - Name: "stable", - Metadata: map[string]string{ - "datastore": "mysql", - "default": "true", - }, + Name: "stable", + Metadata: map[string]string{ + "datastore": "mysql", + "default": "true", }, Nodes: releases, Edges: map[string][]string{ @@ -233,23 +195,12 @@ func spannerChannel() updates.Channel { {ID: "v1.10.0", Tag: "v1.10.0", Migration: "add-metadata-and-counters"}, {ID: "v1.9.0", Tag: "v1.9.0", Migration: "add-metadata-and-counters"}, {ID: "v1.8.0", Tag: "v1.8.0", Migration: "add-metadata-and-counters"}, - {ID: "v1.7.1", Tag: "v1.7.1", Migration: "add-metadata-and-counters"}, - {ID: "v1.7.0", Tag: "v1.7.0", Migration: "add-metadata-and-counters"}, - {ID: "v1.6.0", Tag: "v1.6.0", Migration: "add-metadata-and-counters"}, - {ID: "v1.5.0", Tag: "v1.5.0", Migration: "initial"}, - {ID: "v1.4.0", Tag: "v1.4.0", Migration: "initial"}, - {ID: "v1.3.0", Tag: "v1.3.0", Migration: "initial"}, - {ID: "v1.2.0", Tag: "v1.2.0", Migration: "initial"}, - {ID: "v1.1.0", Tag: "v1.1.0", Migration: "initial"}, - {ID: "v1.0.0", Tag: "v1.0.0", Migration: "initial"}, } return updates.Channel{ - ChannelID: updates.ChannelID{ - Name: "stable", - Metadata: map[string]string{ - "datastore": "spanner", - "default": "true", - }, + Name: "stable", + Metadata: map[string]string{ + "datastore": "spanner", + "default": "true", }, Nodes: releases, Edges: map[string][]string{ @@ -263,15 +214,6 @@ func spannerChannel() updates.Channel { "v1.10.0": {"v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, "v1.9.0": {"v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, "v1.8.0": {"v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, - "v1.7.1": {"v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, - "v1.7.0": {"v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, - "v1.6.0": {"v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, - "v1.5.0": {"v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, - "v1.4.0": {"v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, - "v1.3.0": {"v1.4.0", "v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, - "v1.2.0": {"v1.3.0", "v1.4.0", "v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, - "v1.1.0": {"v1.2.0", "v1.3.0", "v1.4.0", "v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, - "v1.0.0": {"v1.1.0", "v1.2.0", "v1.3.0", "v1.4.0", "v1.5.0", "v1.6.0", "v1.7.1", "v1.8.0", "v1.9.0", "v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.1", "v1.15.0", "v1.16.0", "v1.16.1"}, }, } } diff --git a/tools/go.mod b/tools/go.mod index a75f77a5..758a50ab 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -4,7 +4,6 @@ go 1.19 require ( github.com/authzed/spicedb-operator v0.0.0-00010101000000-000000000000 - github.com/google/go-github/v43 v43.0.0 sigs.k8s.io/yaml v1.3.0 ) @@ -21,7 +20,6 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/go-cmp v0.5.9 // indirect - github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -30,7 +28,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - golang.org/x/crypto v0.1.0 // indirect golang.org/x/exp v0.0.0-20220823124025-807a23277127 // indirect golang.org/x/net v0.5.0 // indirect golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect diff --git a/tools/go.sum b/tools/go.sum index 46a7cc31..6ffdc913 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -50,14 +50,9 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v43 v43.0.0 h1:y+GL7LIsAIF2NZlJ46ZoC/D1W1ivZasT0lnWHMYPZ+U= -github.com/google/go-github/v43 v43.0.0/go.mod h1:ZkTvvmCXBvsfPpTHXnH/d2hP9Y0cTbvN9kr5xqyXOIc= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -108,8 +103,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20220823124025-807a23277127 h1:S4NrSKDfihhl3+4jSTgwoIevKxX9p7Iv9x++OEIptDo= golang.org/x/exp v0.0.0-20220823124025-807a23277127/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= diff --git a/tools/validate-graph/main.go b/tools/validate-graph/main.go deleted file mode 100644 index 1e1c51ab..00000000 --- a/tools/validate-graph/main.go +++ /dev/null @@ -1,119 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - - "github.com/authzed/controller-idioms/hash" - "golang.org/x/exp/maps" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apimachinery/pkg/util/yaml" - - "github.com/authzed/spicedb-operator/pkg/config" - "github.com/authzed/spicedb-operator/pkg/updates" -) - -// Usage: main.go - -// Every node and edge in the existing graph is assumed to be correct -// Every node and edge in the proposed graph that is not in the existing -// graph will be validated. - -//go:generate go run main.go ../../default-operator-config.yaml - -const ( - githubNamespace = "authzed" - githubRepository = "spicedb" -) - -func main() { - if len(os.Args) != 2 { - fmt.Println("must provide path to update graph file") - os.Exit(1) - } - - // load proposed graph - proposedGraphFile, err := os.Open(os.Args[1]) - if err != nil { - log.Fatalf("error loading graph file: %v", err) - } - decoder := yaml.NewYAMLOrJSONDecoder(proposedGraphFile, 100) - var proposedGraph config.OperatorConfig - if err := decoder.Decode(&proposedGraph); err != nil { - log.Fatalf("error decoding graph file: %v", err) - } - - existingGraph := config.NewOperatorConfig() - - // if we have been provided a validation record, load it - if len(os.Args) > 2 { - existingGraphFile, err := os.Open(os.Args[1]) - if err != nil { - log.Fatalf("error loading graph file: %v", err) - } - decoder = yaml.NewYAMLOrJSONDecoder(existingGraphFile, 100) - if err := decoder.Decode(&existingGraph); err != nil { - log.Fatalf("error decoding graph file: %v", err) - } - } - - errs := make([]error, 0) - // validate every node that's not already validated - for _, proposed := range proposedGraph.Channels { - for _, existing := range existingGraph.Channels { - if proposed.EqualIdentity(existing) { - // validate new nodes - - for _, node := range proposed.Nodes { - - } - - // validate new edges - } - } - } - - // validate every edge that's not already validated -} - -func newNodes(proposed, existing []updates.State) []updates.State { - proposedSet := hash.NewSet(proposed...) - existingSet := hash.NewSet(existing...) - return maps.Values(proposedSet.SetDifference(existingSet)) -} - -func newEdges(proposed, existing updates.EdgeSet) updates.EdgeSet { - -} - -func validateNode(state updates.State) error { - if len(state.ID) == 0 { - return fmt.Errorf("node missing identifier: %v", state) - } - if len(state.Tag) == 0 && len(state.Digest) == 0 { - return fmt.Errorf("node must specify image or tag: %v", state) - } - - // validate tag if present - if len(state.Tag) > 0 { - - } - - // validate digest if present - if len(state.Digest) > 0 { - - } - - // validate tag matches digest if both are specified - - // validate that migration is valid - - // validate that migration is head if phase == "" - - // validate that phase is valid for that binary - - // validate that - - return nil -} diff --git a/validated-update-graph.yaml b/validated-update-graph.yaml new file mode 100644 index 00000000..01555993 --- /dev/null +++ b/validated-update-graph.yaml @@ -0,0 +1,2 @@ +channels: [] +imageName: ghcr.io/authzed/spicedb