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

Integrate autoscaler with VerticaDB #195

Merged
merged 32 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
934789b
Add autoscaler CR scafolding
Mar 10, 2022
c8de100
More autoscaler CR updates
Mar 11, 2022
03d0f73
Finish subcluster resize
Mar 14, 2022
7fb4cb7
Add autoscale e2e test
Mar 15, 2022
de1ba18
Merge branch 'main' into autoscaler
Mar 21, 2022
4b17cfe
Fix merge conflicts
Mar 22, 2022
98fc53d
Merge branch 'main' into autoscaler2
Mar 23, 2022
6c18e7a
Add scaling by subcluster
Mar 24, 2022
48bb4b0
Rename autoscale-sanity to autoscale-by-pod
Mar 24, 2022
0f1f015
e2e test
Mar 25, 2022
ab311ce
Fixes for e2e test
Mar 25, 2022
30d4d06
Fix scorecard tests
Mar 25, 2022
20c2352
Status changes
Mar 28, 2022
212b5b9
Merge branch 'main' into autoscaler
Mar 29, 2022
21d6356
Handling pending pods better
Mar 29, 2022
aa9dd52
Add vertica category to kubectl
Mar 31, 2022
bc07fcd
Expand label selector
Mar 31, 2022
584b4fc
Add webhook
Mar 31, 2022
0e8c2c6
Add ability to use existing subcluster as template
Apr 1, 2022
8a80d11
Add currentSize and init targetSize
Apr 1, 2022
61e6907
Code cleanup (rename)
Apr 1, 2022
3e2382b
Revert changes for pending pod handling
Apr 4, 2022
dec0300
Seperate autoscaler controller into separate package
Apr 8, 2022
1f8e5ae
Code cleanup
Apr 8, 2022
a686ec0
Code cleanup
Apr 8, 2022
7311fae
Merge branch 'main' into autoscaler
Apr 8, 2022
ecc5426
Minor e2e fixes (#196)
Apr 8, 2022
1103d54
Apply review comments
Apr 11, 2022
56d77cb
Move vas to its own subdir under controllers
Apr 11, 2022
a230daa
Move VerticaDB reconciler to controllers/vdb
Apr 11, 2022
8ae3f31
Merge branch 'main' into autoscaler
Apr 11, 2022
30b2b65
Address review comments
Apr 11, 2022
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ testbin/*
*~

# Omit some fully generated files
config/crd/bases/vertica.com_verticadbs.yaml
config/crd/bases/*.yaml
config/rbac/role.yaml
api/v1beta1/zz_generated.deepcopy.go

Expand Down
11 changes: 11 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,15 @@ resources:
defaulting: true
validation: true
webhookVersion: v1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: vertica.com
kind: VerticaAutoscaler
path: github.com/vertica/vertica-kubernetes/api/v1beta1
version: v1beta1
webhooks:
validation: true
webhookVersion: v1
version: "3"
10 changes: 9 additions & 1 deletion api/v1beta1/groupversion_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@ import (
"sigs.k8s.io/controller-runtime/pkg/scheme"
)

const (
Group = "vertica.com"
Version = "v1beta1"

VerticaDBKind = "VerticaDB"
VerticaAutoscalerKind = "VerticaAutoscaler"
)

var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "vertica.com", Version: "v1beta1"}
GroupVersion = schema.GroupVersion{Group: Group, Version: Version}

// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
Expand Down
200 changes: 200 additions & 0 deletions api/v1beta1/verticaautoscaler_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
Copyright [2021-2022] Micro Focus or one of its affiliates.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// nolint:lll
package v1beta1

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

// VerticaAutoscalerSpec defines the desired state of VerticaAutoscaler
type VerticaAutoscalerSpec struct {
// Important: Run "make" to regenerate code after modifying this file

// +operator-sdk:csv:customresourcedefinitions:type=spec
// +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors="urn:alm:descriptor:com.tectonic.ui:text"
// The name of the VerticaDB CR that this autoscaler is defined for. The
// VerticaDB object must exist in the same namespace as this object.
VerticaDBName string `json:"verticaDBName,omitempty"`

// +operator-sdk:csv:customresourcedefinitions:type=spec
// +kubebuilder:default:="Subcluster"
// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:select:Pod","urn:alm:descriptor:com.tectonic.ui:select:Subcluster"}
// This defines how the scaling will happen. This can be one of the following:
// - Subcluster: Scaling will be achieved by creating or deleting entire subclusters.
// The template for new subclusters are either the template if filled out
// or an existing subcluster that matches the service name.
// - Pod: Only increase or decrease the size of an existing subcluster.
// If multiple subclusters are selected by the serviceName, this will grow
// the last subcluster only.
ScalingGranularity ScalingGranularityType `json:"scalingGranularity"`

// +operator-sdk:csv:customresourcedefinitions:type=spec
// +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors="urn:alm:descriptor:com.tectonic.ui:text"
// This acts as a selector for the subclusters that are being scaled together.
// Each subcluster has a service name field, which if omitted is the same
// name as the subcluster name. Multiple subclusters that have the same
// service name use the same service object.
ServiceName string `json:"serviceName"`

// +operator-sdk:csv:customresourcedefinitions:type=spec
// +kubebuilder:validation:Optional
// When the scaling granularity is Subcluster, this field defines a template
// to use for when a new subcluster needs to be created. If size is 0, then
// the operator will use an existing subcluster to use as the template. If
// size is > 0, the service name must match the serviceName parameter. The
// name of the new subcluster is always auto generated. If the name is set
// here it will be used as a prefix for the new subcluster. Otherwise, we
// use the name of this VerticaAutoscaler object as a prefix for all
// subclusters.
Template Subcluster `json:"template"`

// +operator-sdk:csv:customresourcedefinitions:type=spec
// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors="urn:alm:descriptor:com.tectonic.ui:podCount"
// This is the total pod count for all subclusters that match the
// serviceName. Changing this value may trigger a change in the
// VerticaDB that is associated with this object. This value is generally
// left as zero. It will get initialized in the operator and then modified
// via the /scale subresource.
TargetSize int32 `json:"targetSize"`
}

type ScalingGranularityType string

const (
PodScalingGranularity = "Pod"
SubclusterScalingGranularity = "Subcluster"
)

// VerticaAutoscalerStatus defines the observed state of VerticaAutoscaler
type VerticaAutoscalerStatus struct {
// +operator-sdk:csv:customresourcedefinitions:type=status
// The total number of times the operator has scaled up/down the VerticaDB.
ScalingCount int `json:"scalingCount"`

// +operator-sdk:csv:customresourcedefinitions:type=status
// The observed size of all pods that are routed through the service name.
CurrentSize int32 `json:"currentSize"`

// +operator-sdk:csv:customresourcedefinitions:type=status
// The selector used to find all of the pods for this autoscaler.
Selector string `json:"selector"`

// +operator-sdk:csv:customresourcedefinitions:type=status
// Conditions for VerticaAutoscaler
Conditions []VerticaAutoscalerCondition `json:"conditions,omitempty"`
}

// VerticaAutoscalerCondition defines condition for VerticaAutoscaler
type VerticaAutoscalerCondition struct {
// +operator-sdk:csv:customresourcedefinitions:type=status
// Type is the type of the condition
Type VerticaAutoscalerConditionType `json:"type"`

// +operator-sdk:csv:customresourcedefinitions:type=status
// Status is the status of the condition
// can be True, False or Unknown
Status corev1.ConditionStatus `json:"status"`

// +operator-sdk:csv:customresourcedefinitions:type=status
// Last time the condition transitioned from one status to another.
// +optional
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
}

type VerticaAutoscalerConditionType string

const (
// TargetSizeInitialized indicates whether the operator has initialized targetSize in the spec
TargetSizeInitialized VerticaAutoscalerConditionType = "TargetSizeInitialized"
)

// Fixed index entries for each condition.
const (
TargetSizeInitializedIndex = iota
)

//+kubebuilder:object:root=true
//+kubebuilder:resource:categories=all;vertica,shortName=vas
//+kubebuilder:subresource:status
//+kubebuilder:subresource:scale:specpath=.spec.targetSize,statuspath=.status.currentSize,selectorpath=.status.selector
//+kubebuilder:printcolumn:name="Granularity",type="string",JSONPath=".spec.scalingGranularity"
//+kubebuilder:printcolumn:name="Current Size",type="integer",JSONPath=".status.currentSize"
//+kubebuilder:printcolumn:name="Target Size",type="integer",JSONPath=".spec.targetSize"
//+kubebuilder:printcolumn:name="Scaling Count",type="integer",JSONPath=".status.scalingCount"
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
//+operator-sdk:csv:customresourcedefinitions:resources={{VerticaDB,vertica.com/v1beta1,""}}

// VerticaAutoscaler is the Schema for the verticaautoscalers API
type VerticaAutoscaler struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec VerticaAutoscalerSpec `json:"spec,omitempty"`
Status VerticaAutoscalerStatus `json:"status,omitempty"`
}

//+kubebuilder:object:root=true

// VerticaAutoscalerList contains a list of VerticaAutoscaler
type VerticaAutoscalerList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []VerticaAutoscaler `json:"items"`
}

func init() {
SchemeBuilder.Register(&VerticaAutoscaler{}, &VerticaAutoscalerList{})
}

// MakeVASName is a helper that creates a sample name for test purposes
func MakeVASName() types.NamespacedName {
return types.NamespacedName{Name: "vertica-vas-sample", Namespace: "default"}
}

// MakeVAS is a helper that constructs a fully formed VerticaAutoscaler struct using the sample name.
// This is intended for test purposes.
func MakeVAS() *VerticaAutoscaler {
vasNm := MakeVASName()
vdbNm := MakeVDBName()
return &VerticaAutoscaler{
TypeMeta: metav1.TypeMeta{
APIVersion: GroupVersion.String(),
Kind: VerticaAutoscalerKind,
},
ObjectMeta: metav1.ObjectMeta{
Name: vasNm.Name,
Namespace: vasNm.Namespace,
UID: "abcdef-ghi",
Annotations: make(map[string]string),
},
Spec: VerticaAutoscalerSpec{
VerticaDBName: vdbNm.Name,
ScalingGranularity: "Pod",
ServiceName: "sc1",
},
}
}

// CanUseTemplate returns true if we can use the template provided in the spec
func (v *VerticaAutoscaler) CanUseTemplate() bool {
return v.Spec.Template.Size > 0
}
123 changes: 123 additions & 0 deletions api/v1beta1/verticaautoscaler_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
Copyright [2021-2022] Micro Focus or one of its affiliates.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

//nolint:lll
package v1beta1

import (
"fmt"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)

// log is for logging in this package.
var verticaautoscalerlog = logf.Log.WithName("verticaautoscaler-resource")

func (v *VerticaAutoscaler) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(v).
Complete()
}

//+kubebuilder:webhook:path=/mutate-vertica-com-v1beta1-verticaautoscaler,mutating=true,failurePolicy=fail,sideEffects=None,groups=vertica.com,resources=verticaautoscalers,verbs=create;update,versions=v1beta1,name=mverticaautoscaler.kb.io,admissionReviewVersions=v1
var _ webhook.Defaulter = &VerticaDB{}

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (v *VerticaAutoscaler) Default() {
verticaautoscalerlog.Info("default", "name", v.Name)

if v.Spec.Template.ServiceName == "" {
v.Spec.Template.ServiceName = v.Spec.ServiceName
}
}

//+kubebuilder:webhook:path=/validate-vertica-com-v1beta1-verticaautoscaler,mutating=false,failurePolicy=fail,sideEffects=None,groups=vertica.com,resources=verticaautoscalers,verbs=create;update,versions=v1beta1,name=vverticaautoscaler.kb.io,admissionReviewVersions=v1
var _ webhook.Validator = &VerticaAutoscaler{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (v *VerticaAutoscaler) ValidateCreate() error {
verticaautoscalerlog.Info("validate create", "name", v.Name)

allErrs := v.validateSpec(true)
if len(allErrs) == 0 {
return nil
}
return apierrors.NewInvalid(schema.GroupKind{Group: Group, Kind: VerticaAutoscalerKind}, v.Name, allErrs)
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (v *VerticaAutoscaler) ValidateUpdate(old runtime.Object) error {
verticaautoscalerlog.Info("validate update", "name", v.Name)

allErrs := v.validateSpec(false)
if len(allErrs) == 0 {
return nil
}
return apierrors.NewInvalid(schema.GroupKind{Group: Group, Kind: VerticaAutoscalerKind}, v.Name, allErrs)
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (v *VerticaAutoscaler) ValidateDelete() error {
verticaautoscalerlog.Info("validate delete", "name", v.Name)

return nil
}

// validateSpec will validate the current VerticaAutoscaler to see if it is valid
func (v *VerticaAutoscaler) validateSpec(isCreate bool) field.ErrorList {
allErrs := field.ErrorList{}
allErrs = v.validateScalingGranularity(allErrs)
allErrs = v.validateSubclusterTemplate(allErrs, isCreate)
return allErrs
}

// validateScalingGranularity will check if the scalingGranularity field is valid
func (v *VerticaAutoscaler) validateScalingGranularity(allErrs field.ErrorList) field.ErrorList {
switch v.Spec.ScalingGranularity {
case PodScalingGranularity, SubclusterScalingGranularity:
return allErrs
default:
err := field.Invalid(field.NewPath("spec").Child("scalingGranularity"),
v.Spec.ScalingGranularity,
fmt.Sprintf("scalingGranularity must be set to either %s or %s",
SubclusterScalingGranularity,
PodScalingGranularity))
return append(allErrs, err)
}
}

// validateSubclusterTemplate will validate the subcluster template
func (v *VerticaAutoscaler) validateSubclusterTemplate(allErrs field.ErrorList, isCreate bool) field.ErrorList {
pathPrefix := field.NewPath("spec").Child("template")
// We have a defaulter that sets the service name in template to match
// spec.serviceName. So we only need to check for differences if this is an
// update or a create but we set something.
if (!isCreate || v.Spec.Template.ServiceName != "") &&
v.Spec.Template.ServiceName != v.Spec.ServiceName {
err := field.Invalid(pathPrefix.Child("serviceName"),
v.Spec.Template.ServiceName,
"The serviceName in the subcluster template must match spec.serviceName")
allErrs = append(allErrs, err)
}

return allErrs
}
Loading