From e3ad798e7b61c8bda4105774871787f738ee95d3 Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Tue, 15 Nov 2022 10:49:32 -0500 Subject: [PATCH] show available versions in the status of the spicedbcluster --- .github/workflows/build-test.yaml | 5 - e2e/cluster_test.go | 2 + pkg/apis/authzed/v1alpha1/types.go | 45 +++++++++ pkg/controller/validate_config.go | 122 +++++++++++++++++++++---- pkg/controller/validate_config_test.go | 2 + 5 files changed, 151 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index f6548404..97d5aa80 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -75,16 +75,11 @@ jobs: file: "spicedb/Dockerfile" tags: "spicedb:dev,spicedb:updated" outputs: "type=docker,dest=/tmp/image.tar" - - name: "Tag Test v1.13.0 image" - run: | - docker pull ghcr.io/authzed/spicedb:v1.13.0 - docker tag ghcr.io/authzed/spicedb:v1.13.0 spicedb:v1.13.0 - name: "Run Ginkgo Tests" run: "go run github.com/onsi/ginkgo/v2/ginkgo --skip-package ./spicedb --tags=e2e -r --procs=2 -v --randomize-all --randomize-suites --fail-on-pending --fail-fast --no-color --race --trace --json-report=report.json -- -v=4" env: PROVISION: "true" ARCHIVES: "/tmp/image.tar" - IMAGES: "spicedb:v1.13.0" - uses: "docker/build-push-action@v2" with: context: "./" diff --git a/e2e/cluster_test.go b/e2e/cluster_test.go index 31cf7dc1..9d0d2a4e 100644 --- a/e2e/cluster_test.go +++ b/e2e/cluster_test.go @@ -1005,6 +1005,8 @@ var _ = Describe("SpiceDBClusters", func() { fetched, err := typed.UnstructuredObjToTypedObj[*v1alpha1.SpiceDBCluster](clusterUnst) g.Expect(err).To(Succeed()) g.Expect(len(fetched.Status.Conditions)).To(BeZero()) + GinkgoWriter.Println(fetched.Status) + g.Expect(len(fetched.Status.AvailableVersions)).ToNot(BeZero(), "status should show available updates") }).Should(Succeed()) // once the cluster is running at the initial version, update the target version diff --git a/pkg/apis/authzed/v1alpha1/types.go b/pkg/apis/authzed/v1alpha1/types.go index 1310b644..c4d9881e 100644 --- a/pkg/apis/authzed/v1alpha1/types.go +++ b/pkg/apis/authzed/v1alpha1/types.go @@ -3,6 +3,7 @@ package v1alpha1 import ( "encoding/json" + "golang.org/x/exp/slices" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -120,12 +121,56 @@ type ClusterStatus struct { Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` } +func (s ClusterStatus) Equals(other ClusterStatus) bool { + if s.ObservedGeneration != other.ObservedGeneration { + return false + } + if s.TargetMigrationHash != other.TargetMigrationHash { + return false + } + if s.CurrentMigrationHash != other.CurrentMigrationHash { + return false + } + if s.SecretHash != other.SecretHash { + return false + } + if s.Image != other.Image { + return false + } + if s.Migration != other.Migration { + return false + } + if s.Phase != other.Phase { + return false + } + if !s.CurrentVersion.Equals(other.CurrentVersion) { + return false + } + if !slices.Equal(s.AvailableVersions, other.AvailableVersions) { + return false + } + if !slices.Equal(s.Conditions, other.Conditions) { + return false + } + return true +} + type SpiceDBVersion struct { Name string `json:"name"` Channel string `json:"channel"` Description string `json:"description,omitempty"` } +func (v *SpiceDBVersion) Equals(other *SpiceDBVersion) bool { + if v == other { + return true + } + if v != nil && other != nil && v.Name == other.Name && v.Channel == other.Channel { + return true + } + return false +} + // SpiceDBClusterList is a list of SpiceDBCluster resources // // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/controller/validate_config.go b/pkg/controller/validate_config.go index cf62dc92..b1e9968b 100644 --- a/pkg/controller/validate_config.go +++ b/pkg/controller/validate_config.go @@ -4,7 +4,9 @@ import ( "context" "encoding/json" + "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/record" @@ -13,6 +15,7 @@ import ( "github.com/authzed/spicedb-operator/pkg/apis/authzed/v1alpha1" "github.com/authzed/spicedb-operator/pkg/config" + "github.com/authzed/spicedb-operator/pkg/updates" ) const EventInvalidSpiceDBConfig = "InvalidSpiceDBConfig" @@ -73,27 +76,31 @@ func (c *ValidateConfigHandler) Handle(ctx context.Context) { } ctx = CtxMigrationHash.WithValue(ctx, migrationHash) + computedStatus := v1alpha1.ClusterStatus{ + ObservedGeneration: currentStatus.GetGeneration(), + TargetMigrationHash: migrationHash, + CurrentMigrationHash: currentStatus.Status.CurrentMigrationHash, + SecretHash: currentStatus.Status.SecretHash, + Image: validatedConfig.TargetSpiceDBImage, + Migration: validatedConfig.TargetMigration, + Phase: validatedConfig.TargetPhase, + CurrentVersion: validatedConfig.SpiceDBVersion, + Conditions: currentStatus.GetStatusConditions(), + } + if validatedConfig.SpiceDBVersion != nil { + computedStatus.AvailableVersions = c.getAvailableVersions(ctx, operatorConfig.UpdateGraph, *validatedConfig.SpiceDBVersion, validatedConfig.DatastoreEngine) + } + meta.RemoveStatusCondition(&computedStatus.Conditions, v1alpha1.ConditionValidatingFailed) + meta.RemoveStatusCondition(&computedStatus.Conditions, v1alpha1.ConditionTypeValidating) + if warningCondition != nil { + meta.SetStatusCondition(&computedStatus.Conditions, *warningCondition) + } else { + meta.RemoveStatusCondition(&computedStatus.Conditions, v1alpha1.ConditionTypeConfigWarnings) + } + // Remove invalid config status and set image and hash - if currentStatus.IsStatusConditionTrue(v1alpha1.ConditionValidatingFailed) || - currentStatus.IsStatusConditionTrue(v1alpha1.ConditionTypeValidating) || - currentStatus.Status.Image != validatedConfig.TargetSpiceDBImage || - currentStatus.Status.TargetMigrationHash != migrationHash || - currentStatus.IsStatusConditionChanged(v1alpha1.ConditionTypeConfigWarnings, warningCondition) || - // TODO: this should deref and check the values if they're not nil - currentStatus.Status.CurrentVersion != validatedConfig.SpiceDBVersion { - currentStatus.RemoveStatusCondition(v1alpha1.ConditionValidatingFailed) - currentStatus.Status.CurrentVersion = validatedConfig.SpiceDBVersion - currentStatus.Status.Image = validatedConfig.TargetSpiceDBImage - currentStatus.Status.TargetMigrationHash = migrationHash - currentStatus.Status.ObservedGeneration = currentStatus.GetGeneration() - currentStatus.Status.Phase = validatedConfig.TargetPhase - currentStatus.Status.Migration = validatedConfig.TargetMigration - if warningCondition != nil { - currentStatus.SetStatusCondition(*warningCondition) - } else { - currentStatus.RemoveStatusCondition(v1alpha1.ConditionTypeConfigWarnings) - } - currentStatus.RemoveStatusCondition(v1alpha1.ConditionTypeValidating) + if !currentStatus.Status.Equals(computedStatus) { + currentStatus.Status = computedStatus if err := c.patchStatus(ctx, currentStatus); err != nil { QueueOps.RequeueAPIErr(ctx, err) return @@ -104,3 +111,78 @@ func (c *ValidateConfigHandler) Handle(ctx context.Context) { ctx = CtxClusterStatus.WithValue(ctx, currentStatus) c.next.Handle(ctx) } + +func (c *ValidateConfigHandler) getAvailableVersions(ctx context.Context, graph updates.UpdateGraph, version v1alpha1.SpiceDBVersion, datastore string) []v1alpha1.SpiceDBVersion { + logger := logr.FromContextOrDiscard(ctx) + source, err := graph.SourceForChannel(version.Channel) + if err != nil { + logger.V(4).Error(err, "no source found for channel %q, can't compute available versions", version.Channel) + } + + availableVersions := make([]v1alpha1.SpiceDBVersion, 0) + nextDirect := source.NextDirect(version.Name) + next := source.Next(version.Name) + latest := source.Latest(version.Name) + if len(nextDirect) > 0 { + nextDirectVersion := v1alpha1.SpiceDBVersion{ + Name: nextDirect, + Channel: version.Channel, + Description: "direct update with no migrations", + } + if nextDirect == latest { + nextDirectVersion.Description += ", head of channel" + } + availableVersions = append(availableVersions, nextDirectVersion) + } + if len(next) > 0 && next != nextDirect { + nextVersion := v1alpha1.SpiceDBVersion{ + Name: next, + Channel: version.Channel, + Description: "update will run a migration", + } + if next == latest { + nextVersion.Description += ", head of channel" + } + availableVersions = append(availableVersions, nextVersion) + } + if len(latest) > 0 && next != latest && nextDirect != latest { + availableVersions = append(availableVersions, v1alpha1.SpiceDBVersion{ + Name: latest, + Channel: version.Channel, + Description: "head of the channel, multiple updates will run in sequence", + }) + } + + // check for options in other channels (only show the safest update for + // each available channel) + for _, c := range graph.Channels { + if c.Name == version.Channel { + continue + } + if c.Metadata["datastore"] != datastore { + continue + } + source, err := graph.SourceForChannel(c.Name) + if err != nil { + logger.V(4).Error(err, "no source found for channel %q, can't compute available versions", c.Name) + continue + } + if next := source.NextDirect(version.Name); len(next) > 0 { + availableVersions = append(availableVersions, v1alpha1.SpiceDBVersion{ + Name: next, + Channel: c.Name, + Description: "direct update with no migrations, different channel", + }) + continue + } + if next := source.Next(version.Name); len(next) > 0 { + availableVersions = append(availableVersions, v1alpha1.SpiceDBVersion{ + Name: next, + Channel: c.Name, + Description: "update will run a migration, different channel", + }) + } + } + + return availableVersions +} diff --git a/pkg/controller/validate_config_test.go b/pkg/controller/validate_config_test.go index da0a9943..f22404c6 100644 --- a/pkg/controller/validate_config_test.go +++ b/pkg/controller/validate_config_test.go @@ -40,12 +40,14 @@ func TestValidateConfigHandler(t *testing.T) { name: "valid config, no changes, no warnings", currentStatus: &v1alpha1.SpiceDBCluster{Status: v1alpha1.ClusterStatus{ Image: "image:v1", + Migration: "head", TargetMigrationHash: "ndchdch68dh69h566h56fhb9h5dq", CurrentMigrationHash: "ndchdch68dh69h566h56fhb9h5dq", CurrentVersion: &v1alpha1.SpiceDBVersion{ Name: "v1", Channel: "cockroachdb", }, + AvailableVersions: []v1alpha1.SpiceDBVersion{}, }}, rawConfig: json.RawMessage(`{ "datastoreEngine": "cockroachdb",