Skip to content

Commit

Permalink
first stab at scheduled upgrades
Browse files Browse the repository at this point in the history
  • Loading branch information
agouin committed Oct 18, 2023
1 parent 9d6c29c commit 7d286da
Show file tree
Hide file tree
Showing 13 changed files with 631 additions and 108 deletions.
35 changes: 35 additions & 0 deletions api/v1/cosmosfullnode_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package v1

import (
"fmt"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
Expand Down Expand Up @@ -524,6 +526,39 @@ type ChainSpec struct {
// +kubebuilder:validation:Minimum:=0
// +optional
PrivvalSleepSeconds *int32 `json:"privvalSleepSeconds"`

// DatabaseBackend must match in order to detect the block height
// of the chain prior to starting in order to pick the correct image version.
// options: goleveldb, rocksdb, pebbledb
// Defaults to goleveldb.
// +optional
DatabaseBackend *string `json:"databaseBackend"`

// Versions of the chain and which height they should be applied.
// When provided, the operator will automatically upgrade the chain as it reaches the specified heights.
// If not provided, the operator will not upgrade the chain, and will use the image specified in the pod spec.
// +optional
Versions ChainVersions `json:"versions"`
}

type ChainVersion struct {
// UpgradeHeight is the block height when this version is applied.
UpgradeHeight uint64 `json:"minHeight"`

// Image is the docker image for this version in "repository:tag" format. E.g. busybox:latest.
Image string `json:"image"`
}

type ChainVersions []ChainVersion

func (cv ChainVersions) Validate() error {
lastHeight := uint64(0)
for _, v := range cv {
if v.UpgradeHeight < lastHeight {
return fmt.Errorf("upgrade height %d is less than last upgrade height %d. upgrades must be sequential", v.UpgradeHeight, lastHeight)
}
}
return nil
}

// CometConfig configures the config.toml.
Expand Down
44 changes: 44 additions & 0 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions healtcheck_cmd.go → cmd/healtcheck.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package cmd

import (
"context"
Expand All @@ -14,7 +14,7 @@ import (
"golang.org/x/sync/errgroup"
)

func healthcheckCmd() *cobra.Command {
func HealthCheckCmd() *cobra.Command {
hc := &cobra.Command{
Short: "Start health check probe",
Use: "healthcheck",
Expand Down Expand Up @@ -43,7 +43,7 @@ func startHealthCheckServer(cmd *cobra.Command, args []string) error {
httpClient = &http.Client{Timeout: 30 * time.Second}
cometClient = cosmos.NewCometClient(httpClient)

zlog = zapLogger("info", viper.GetString("log-format"))
zlog = ZapLogger("info", viper.GetString("log-format"))
logger = zapr.NewLogger(zlog)
)
defer func() { _ = zlog.Sync() }()
Expand Down
4 changes: 2 additions & 2 deletions logger.go → cmd/logger.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package cmd

import (
"os"
Expand All @@ -8,7 +8,7 @@ import (
"go.uber.org/zap/zapcore"
)

func zapLogger(level, format string) *zap.Logger {
func ZapLogger(level, format string) *zap.Logger {
config := zap.NewProductionEncoderConfig()
config.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(ts.UTC().Format(time.RFC3339))
Expand Down
153 changes: 153 additions & 0 deletions cmd/versioncheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
Copyright © 2023 Strangelove Crypto, Inc.
*/
package cmd

import (
"fmt"
"os"
"time"

"cosmossdk.io/log"
"cosmossdk.io/store/rootmulti"
dbm "github.com/cosmos/cosmos-db"
"github.com/spf13/cobra"
cosmosv1 "github.com/strangelove-ventures/cosmos-operator/api/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"

flagBackend = "backend"
)

// VersionCheckCmd gets the height of this node and updates the status of the crd.
// It panics if the wrong image is specified for the pod for the height,
// restarting the pod so that the correct image is used from the patched height.
// this command is intended to be run as an init container.
func VersionCheckCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "versioncheck",
Short: "Confirm correct image used for current node height",
Long: `Open the Cosmos SDK chain database, get the height, update the crd status with the height, then check the image for the height and panic if it is incorrect.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("height called")

dataDir := os.Getenv("DATA_DIR")
backend, _ := cmd.Flags().GetString(flagBackend)

s, err := os.Stat(dataDir)
if err != nil {
panic(fmt.Errorf("failed to stat %s: %w", dataDir, err))
}

if !s.IsDir() {
panic(fmt.Errorf("%s is not a directory", dataDir))
}

db, err := dbm.NewDB("application", getBackend(backend), dataDir)
if err != nil {
panic(fmt.Errorf("failed to open db: %w", err))
}

store := rootmulti.NewStore(db, log.NewNopLogger(), nil)

height := store.LatestVersion()
fmt.Printf("%d", height)

nsbz, err := os.ReadFile(namespaceFile)
if err != nil {
panic(fmt.Errorf("failed to read namespace from service account: %w", err))
}
ns := string(nsbz)

config, err := rest.InClusterConfig()
if err != nil {
panic(fmt.Errorf("failed to get in cluster config: %w", err))
}

clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(fmt.Errorf("failed to create kube clientset: %w", err))
}

ctx := cmd.Context()

thisPod, err := clientset.CoreV1().Pods(ns).Get(ctx, os.Getenv("HOSTNAME"), metav1.GetOptions{})
if err != nil {
panic(fmt.Errorf("failed to get this pod: %w", err))
}

cosmosFullNodeName := thisPod.Labels["app.kubernetes.io/name"]

kClient, err := client.New(config, client.Options{})
if err != nil {
panic(fmt.Errorf("failed to create kube client: %w", err))
}

namespacedName := types.NamespacedName{
Namespace: ns,
Name: cosmosFullNodeName,
}

crd := new(cosmosv1.CosmosFullNode)
if err := kClient.Get(ctx, namespacedName, crd); err != nil {
panic(fmt.Errorf("failed to get crd: %w", err))
}

if err := kClient.Status().Patch(
ctx, crd,
client.RawPatch(
types.StrategicMergePatchType,
[]byte(fmt.Sprintf(
`{"syncInfo":{"pods":[{"pod":"%s","time":"%s","height":%d,"inSync": false}]}}`,
thisPod.Name,
time.Now().Format(time.RFC3339),
height,
)),
),
); err != nil {
panic(fmt.Errorf("failed to patch status: %w", err))
}

var image string
for _, v := range crd.Spec.ChainSpec.Versions {
if uint64(height) < v.UpgradeHeight {
break
}
image = v.Image
}

thisPodImage := thisPod.Spec.Containers[0].Image
if thisPodImage != image {
panic(fmt.Errorf("image mismatch for height %d: %s != %s", height, thisPodImage, image))
}

fmt.Printf("Verified correct image for height %d: %s\n", height, image)
},
}

cmd.Flags().StringP(flagBackend, "b", "goleveldb", "Database backend")

return cmd
}

func getBackend(backend string) dbm.BackendType {
switch backend {
case "goleveldb":
return dbm.GoLevelDBBackend
case "memdb":
return dbm.MemDBBackend
case "rocksdb":
return dbm.RocksDBBackend
case "pebbledb":
return dbm.PebbleDBBackend
default:
panic(fmt.Errorf("unknown backend %s", backend))
}
}
28 changes: 28 additions & 0 deletions config/crd/bases/cosmos.strange.love_cosmosfullnodes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ spec:
limits.
type: string
type: object
databaseBackend:
description: 'DatabaseBackend must match in order to detect the
block height of the chain prior to starting in order to pick
the correct image version. options: goleveldb, rocksdb, pebbledb
Defaults to goleveldb.'
type: string
genesisScript:
description: 'Specify shell (sh) script commands to properly download
and save the genesis file. Prefer GenesisURL if the file is
Expand Down Expand Up @@ -310,6 +316,28 @@ spec:
if the snapshot archive is unconventional or requires special
handling.'
type: string
versions:
description: Versions of the chain and which height they should
be applied. When provided, the operator will automatically upgrade
the chain as it reaches the specified heights. If not provided,
the operator will not upgrade the chain, and will use the image
specified in the pod spec.
items:
properties:
image:
description: Image is the docker image for this version
in "repository:tag" format. E.g. busybox:latest.
type: string
minHeight:
description: UpgradeHeight is the block height when this
version is applied.
format: int64
type: integer
required:
- image
- minHeight
type: object
type: array
required:
- app
- binary
Expand Down
Loading

0 comments on commit 7d286da

Please sign in to comment.