Skip to content

Commit

Permalink
Add restore parameters to v1 API (#640)
Browse files Browse the repository at this point in the history
- add `RestorePointPolicy` type in `Spec`
  - `Archive` must be specified if a restore is intended
- one of `ID` and `Index` must be specified (`Index` assumed to be
1-based)
- webhook rules to validate `RestorePointPolicy`
- revivedb_reconciler checks whether restore is supported given the
server version and deployment method
  - must use vclusterops and has server version greater than v24.2.0

---------

Co-authored-by: Roy Paulin <[email protected]>
  • Loading branch information
jizhuoyu and roypaulin authored Dec 20, 2023
1 parent 39a78ff commit d10d15b
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 18 deletions.
12 changes: 12 additions & 0 deletions api/v1/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,3 +572,15 @@ func (v *VerticaDB) GetEncryptSpreadComm() string {
func (v *VerticaDB) IsKSafetyCheckStrict() bool {
return vmeta.IsKSafetyCheckStrict(v.Annotations)
}

// IsValidRestorePointPolicy returns true if the RestorePointPolicy is properly specified,
// i.e., it has a non-empty archive, and either a valid index or a valid id (but not both).
func (r *RestorePointPolicy) IsValidRestorePointPolicy() bool {
return r != nil && r.Archive != "" && ((r.Index > 0) != (r.ID != ""))
}

// IsRestoreEnabled will return whether the vdb is configured to initialize by reviving from
// a restore point in an archive
func (v *VerticaDB) IsRestoreEnabled() bool {
return v.Spec.InitPolicy == CommunalInitPolicyRevive && v.Spec.RestorePoint != nil
}
2 changes: 2 additions & 0 deletions api/v1/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ const (
VcluseropsAsDefaultDeploymentMethodMinVersion = "v24.1.0"
// Starting in v24.1.0, we use server logrotate and not depend on cron job
InDatabaseLogRotateMinVersion = "v24.1.0"
// Starting in v24.2.0, restoring from a restore point in archive is supported.
RestoreSupportedMinVersion = "v24.2.0"
)

// GetVerticaVersionStr returns the vertica version, in string form, that is stored
Expand Down
28 changes: 28 additions & 0 deletions api/v1/verticadb_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ type VerticaDBSpec struct {
// options are to create a new database or revive an existing one.
InitPolicy CommunalInitPolicy `json:"initPolicy"`

// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:fieldDependency:initPolicy:Revive","urn:alm:descriptor:com.tectonic.ui:advanced"}
// Specifies the restore point details to use with this instance of the VerticaDB.
RestorePoint *RestorePointPolicy `json:"restorePoint,omitempty"`

// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:select:Auto","urn:alm:descriptor:com.tectonic.ui:select:Online","urn:alm:descriptor:com.tectonic.ui:select:Offline"}
// +kubebuilder:default:=Auto
Expand Down Expand Up @@ -341,6 +346,8 @@ const (
CommunalInitPolicyCreate = "Create"
// The database in the communal path will be initialized in the VerticaDB
// through a revive_db. The communal path must have a preexisting database.
// This option could also be used to initialize the database by restoring
// from a database archive if restorePoint field is properly specified.
CommunalInitPolicyRevive = "Revive"
// Only schedule pods to run with the vertica container. The bootstrap of
// the database, either create_db or revive_db, is not handled. Use this
Expand All @@ -354,6 +361,27 @@ const (
CommunalInitPolicyCreateSkipPackageInstall = "CreateSkipPackageInstall"
)

// RestorePointPolicy is used to locate the exact archive and restore point within archive
// when a database restore is intended
type RestorePointPolicy struct {
// +operator-sdk:csv:customresourcedefinitions:type=spec
// +kubebuilder:validation:Optional
// Name of the restore archive to use for bootstapping.
// This name refers to an object in the database.
// This must be specified if initPolicy is Revive and a restore is intended.
Archive string `json:"archive,omitempty"`
// +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors="urn:alm:descriptor:com.tectonic.ui:number"
// +kubebuilder:validation:Optional
// The (1-based) index of the restore point in the restore archive to restore from.
// Specify either index or id exclusively; one of these fields is mandatory, but both cannot be used concurrently.
Index int `json:"index,omitempty"`
// +operator-sdk:csv:customresourcedefinitions:type=spec
// +kubebuilder:validation:Optional
// The identifier of the restore point in the restore archive to restore from.
// Specify either index or id exclusively; one of these fields is mandatory, but both cannot be used concurrently.
ID string `json:"id,omitempty"`
}

// Set constant Upgrade Requeue Time
const URTime = 30

Expand Down
28 changes: 28 additions & 0 deletions api/v1/verticadb_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ func (v *VerticaDB) validateVerticaDBSpec() field.ErrorList {
allErrs := v.hasAtLeastOneSC(field.ErrorList{})
allErrs = v.hasValidSubclusterTypes(allErrs)
allErrs = v.hasValidInitPolicy(allErrs)
allErrs = v.hasValidRestorePolicy(allErrs)
allErrs = v.hasValidDBName(allErrs)
allErrs = v.hasPrimarySubcluster(allErrs)
allErrs = v.validateKsafety(allErrs)
Expand Down Expand Up @@ -223,6 +224,33 @@ func (v *VerticaDB) hasValidInitPolicy(allErrs field.ErrorList) field.ErrorList
return allErrs
}

func (v *VerticaDB) hasValidRestorePolicy(allErrs field.ErrorList) field.ErrorList {
if v.IsRestoreEnabled() && !v.Spec.RestorePoint.IsValidRestorePointPolicy() {
if v.Spec.RestorePoint.Archive == "" {
err := field.Invalid(field.NewPath("spec").Child("restorePoint"),
v.Spec.RestorePoint,
fmt.Sprintf("restorePoint is invalid. When initPolicy is set to %s and restorePoint is specified, "+
"archive must be specified.", CommunalInitPolicyRevive))
allErrs = append(allErrs, err)
}
commonErrorMessage := fmt.Sprintf("restorePoint is invalid. When initPolicy is set to %s and restorePoint is specified, "+
"the database will initialize by reviving from a restore point in the specified archive, and thus "+
"either restorePoint.index or restorePoint.id must be specified. ", CommunalInitPolicyRevive)
if v.Spec.RestorePoint.Index == 0 && v.Spec.RestorePoint.ID == "" {
err := field.Invalid(field.NewPath("spec").Child("restorePoint"),
v.Spec.RestorePoint,
commonErrorMessage+"Both fields are currently empty.")
allErrs = append(allErrs, err)
} else if v.Spec.RestorePoint.Index != 0 && v.Spec.RestorePoint.ID != "" {
err := field.Invalid(field.NewPath("spec").Child("restorePoint"),
v.Spec.RestorePoint,
commonErrorMessage+"Both fields are currently specified, which is not allowed.")
allErrs = append(allErrs, err)
}
}
return allErrs
}

func (v *VerticaDB) validateCommunalPath(allErrs field.ErrorList) field.ErrorList {
if v.Spec.InitPolicy == CommunalInitPolicyScheduleOnly {
return allErrs
Expand Down
22 changes: 22 additions & 0 deletions api/v1/verticadb_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,28 @@ var _ = Describe("verticadb_webhook", func() {
validateSpecValuesHaveErr(vdb, true)
})

It("should validate restorePoint when initPolicy is \"Revive\" and a restore is intended", func() {
vdb := createVDBHelper()
vdb.Spec.InitPolicy = "Revive"
vdb.Spec.RestorePoint = &RestorePointPolicy{}
// archive is not provided
validateSpecValuesHaveErr(vdb, true)
vdb.Spec.RestorePoint.Archive = "archive"
// neither id nor index is provided
validateSpecValuesHaveErr(vdb, true)
// both id and index are provided
vdb.Spec.RestorePoint.ID = "id"
vdb.Spec.RestorePoint.Index = 1
validateSpecValuesHaveErr(vdb, true)
// only id is provided
vdb.Spec.RestorePoint.Index = 0
validateSpecValuesHaveErr(vdb, false)
// only index is provided
vdb.Spec.RestorePoint.ID = ""
vdb.Spec.RestorePoint.Index = 1
validateSpecValuesHaveErr(vdb, false)
})

It("should only allow nodePort if serviceType allows for it", func() {
vdb := createVDBHelper()
vdb.Spec.Subclusters[0].ServiceType = v1.ServiceTypeNodePort
Expand Down
20 changes: 20 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.

14 changes: 14 additions & 0 deletions api/v1beta1/verticadb_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ func convertToSpec(src *VerticaDBSpec) v1.VerticaDBSpec {
StartupProbeOverride: src.StartupProbeOverride,
ServiceAccountName: src.ServiceAccountName,
}
if src.RestorePoint != nil {
dst.RestorePoint = &v1.RestorePointPolicy{
Archive: src.RestorePoint.Archive,
Index: src.RestorePoint.Index,
ID: src.RestorePoint.ID,
}
}
for i := range src.ReviveOrder {
dst.ReviveOrder[i] = v1.SubclusterPodCount(src.ReviveOrder[i])
}
Expand Down Expand Up @@ -234,6 +241,13 @@ func convertFromSpec(src *v1.VerticaDB) VerticaDBSpec {
StartupProbeOverride: srcSpec.StartupProbeOverride,
ServiceAccountName: srcSpec.ServiceAccountName,
}
if srcSpec.RestorePoint != nil {
dst.RestorePoint = &RestorePointPolicy{
Archive: srcSpec.RestorePoint.Archive,
Index: srcSpec.RestorePoint.Index,
ID: srcSpec.RestorePoint.ID,
}
}
for i := range srcSpec.ReviveOrder {
dst.ReviveOrder[i] = SubclusterPodCount(srcSpec.ReviveOrder[i])
}
Expand Down
28 changes: 28 additions & 0 deletions api/v1beta1/verticadb_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ type VerticaDBSpec struct {
// options are to create a new database or revive an existing one.
InitPolicy CommunalInitPolicy `json:"initPolicy"`

// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:fieldDependency:initPolicy:Revive","urn:alm:descriptor:com.tectonic.ui:advanced"}
// Specifies the restore point details to use with this instance of the VerticaDB.
RestorePoint *RestorePointPolicy `json:"restorePoint,omitempty"`

// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:select:Auto","urn:alm:descriptor:com.tectonic.ui:select:Online","urn:alm:descriptor:com.tectonic.ui:select:Offline"}
// +kubebuilder:default:=Auto
Expand Down Expand Up @@ -405,6 +410,8 @@ const (
CommunalInitPolicyCreate = "Create"
// The database in the communal path will be initialized in the VerticaDB
// through a revive_db. The communal path must have a preexisting database.
// This option could also be used to initialize the database by restoring
// from a database archive if restorePoint field is properly specified.
CommunalInitPolicyRevive = "Revive"
// Only schedule pods to run with the vertica container. The bootstrap of
// the database, either create_db or revive_db, is not handled. Use this
Expand All @@ -418,6 +425,27 @@ const (
CommunalInitPolicyCreateSkipPackageInstall = "CreateSkipPackageInstall"
)

// RestorePointPolicy is used to locate the exact archive and restore point within archive
// when a database restore is intended
type RestorePointPolicy struct {
// +operator-sdk:csv:customresourcedefinitions:type=spec
// +kubebuilder:validation:Optional
// Name of the restore archive to use for bootstapping.
// This name refers to an object in the database.
// This must be specified if initPolicy is Revive and a restore is intended.
Archive string `json:"archive,omitempty"`
// +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors="urn:alm:descriptor:com.tectonic.ui:number"
// +kubebuilder:validation:Optional
// The (1-based) index of the restore point in the restore archive to restore from.
// Specify either index or id exclusively; one of these fields is mandatory, but both cannot be used concurrently.
Index int `json:"index,omitempty"`
// +operator-sdk:csv:customresourcedefinitions:type=spec
// +kubebuilder:validation:Optional
// The identifier of the restore point in the restore archive to restore from.
// Specify either index or id exclusively; one of these fields is mandatory, but both cannot be used concurrently.
ID string `json:"id,omitempty"`
}

// Set constant Upgrade Requeue Time
const URTime = 30

Expand Down
33 changes: 31 additions & 2 deletions pkg/controllers/vdb/revivedb_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package vdb

import (
"context"
"errors"
"fmt"
"time"

Expand All @@ -35,8 +36,10 @@ import (
"github.com/vertica/vertica-kubernetes/pkg/vadmin"
"github.com/vertica/vertica-kubernetes/pkg/vadmin/opts/describedb"
"github.com/vertica/vertica-kubernetes/pkg/vadmin/opts/revivedb"
"golang.org/x/text/cases"
"golang.org/x/text/language"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
Expand Down Expand Up @@ -80,6 +83,13 @@ func (r *ReviveDBReconciler) Reconcile(ctx context.Context, _ *ctrl.Request) (ct
return ctrl.Result{}, nil
}

// Check if restoring from a restore point is supported
if r.Vdb.IsRestoreEnabled() {
if err := r.hasCompatibleVersionForRestore(); err != nil {
return ctrl.Result{}, err
}
}

// The remaining revive_db logic is driven from GenericDatabaseInitializer.
// This exists to creation an abstraction that is common with create_db.
g := GenericDatabaseInitializer{
Expand All @@ -96,6 +106,25 @@ func (r *ReviveDBReconciler) Reconcile(ctx context.Context, _ *ctrl.Request) (ct
return g.checkAndRunInit(ctx)
}

func (r *ReviveDBReconciler) hasCompatibleVersionForRestore() error {
vinf, err := r.Vdb.MakeVersionInfoCheck()
if err != nil {
return err
}
if !vmeta.UseVClusterOps(r.Vdb.Annotations) || !vinf.IsEqualOrNewer(vapi.RestoreSupportedMinVersion) {
errMsg := fmt.Sprintf("restoring from a restore point is unsupported in ReviveDB "+
"given the current server version and deployment method, "+
"make sure that a server version equal to or above %s is used and deployment method is set to vcluster-ops",
vapi.RestoreSupportedMinVersion)
// Format the event message by capitalizing the first letter
caser := cases.Title(language.English)
eventMsg := caser.String(errMsg)
r.VRec.Event(r.Vdb, corev1.EventTypeWarning, events.ReviveDBRestoreUnsupported, eventMsg)
return errors.New(errMsg)
}
return nil
}

// execCmd will do the actual execution of revive DB.
// This handles logging of necessary events.
func (r *ReviveDBReconciler) execCmd(ctx context.Context, initiatorPod types.NamespacedName,
Expand Down Expand Up @@ -301,7 +330,7 @@ func (r *ReviveDBReconciler) runRevivePlanner(ctx context.Context, op string) (c
err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
vdb := &vapi.VerticaDB{}
if retryErr := r.VRec.Client.Get(ctx, nm, vdb); retryErr != nil {
if errors.IsNotFound(retryErr) {
if apierrors.IsNotFound(retryErr) {
r.Log.Info("VerticaDB resource not found. Ignoring since object must be deleted")
return nil
}
Expand Down
35 changes: 35 additions & 0 deletions pkg/controllers/vdb/revivedb_reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ package vdb

import (
"context"
"errors"
"fmt"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
vapi "github.com/vertica/vertica-kubernetes/api/v1"
"github.com/vertica/vertica-kubernetes/pkg/cmds"
vmeta "github.com/vertica/vertica-kubernetes/pkg/meta"
"github.com/vertica/vertica-kubernetes/pkg/names"
"github.com/vertica/vertica-kubernetes/pkg/reviveplanner"
"github.com/vertica/vertica-kubernetes/pkg/reviveplanner/atparser"
Expand All @@ -48,6 +51,38 @@ var _ = Describe("revivedb_reconcile", func() {
Expect(len(fpr.Histories)).Should(Equal(0))
})

It("should fail if restore is intended but image version or deployment method is invalid", func() {
vdb := vapi.MakeVDB()
vdb.Spec.InitPolicy = vapi.CommunalInitPolicyRevive
vdb.Annotations[vmeta.VClusterOpsAnnotation] = vmeta.VClusterOpsAnnotationTrue
vdb.Spec.RestorePoint = &vapi.RestorePointPolicy{}
vdb.Spec.RestorePoint.Archive = "archive"
vdb.Spec.RestorePoint.Index = 1

fpr := &cmds.FakePodRunner{}
pfacts := MakePodFacts(vdbRec, fpr)
dispatcher := vdbRec.makeDispatcher(logger, vdb, fpr, TestPassword)
r := MakeReviveDBReconciler(vdbRec, logger, vdb, fpr, &pfacts, dispatcher)

errMsg := fmt.Sprintf("restoring from a restore point is unsupported in ReviveDB "+
"given the current server version and deployment method, "+
"make sure that a server version equal to or above %s is used and deployment method is set to vcluster-ops",
vapi.RestoreSupportedMinVersion)

// Wrong image version
vdb.Annotations[vmeta.VersionAnnotation] = "v24.1.0"
res, err := r.Reconcile(ctx, &ctrl.Request{})
Expect(res).Should(Equal(ctrl.Result{}))
Expect(err).Should(MatchError(errors.New(errMsg)))

// Wrong deployment method
vdb.Annotations[vmeta.VersionAnnotation] = "v24.2.0"
vdb.Annotations[vmeta.VClusterOpsAnnotation] = vmeta.VClusterOpsAnnotationFalse
res, err = r.Reconcile(ctx, &ctrl.Request{})
Expect(res).Should(Equal(ctrl.Result{}))
Expect(err).Should(MatchError(errors.New(errMsg)))
})

It("should call revive_db since no db exists", func() {
vdb := vapi.MakeVDB()
vdb.Spec.InitPolicy = vapi.CommunalInitPolicyRevive
Expand Down
1 change: 1 addition & 0 deletions pkg/events/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
ReviveDBNotFound = "ReviveDBNotFound"
ReviveDBPermissionDenied = "ReviveDBPermissionDenied"
ReviveDBNodeCountMismatch = "ReviveDBNodeCountMismatch"
ReviveDBRestoreUnsupported = "ReviveDBRestoreUnsupported"
ReviveOrderBad = "ReviveOrderBad"
ObjectNotFound = "ObjectNotFound"
CommunalCredsWrongKey = "CommunalCredsWrongKey" //nolint:gosec
Expand Down
Loading

0 comments on commit d10d15b

Please sign in to comment.