Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add restore parameters to v1 API #640

Merged
merged 6 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
jizhuoyu marked this conversation as resolved.
Show resolved Hide resolved
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"`
jizhuoyu marked this conversation as resolved.
Show resolved Hide resolved

// +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"
jizhuoyu marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading