diff --git a/apis/v1/groupversion_info.go b/apis/v1/groupversion_info.go index f71dd7295a..219afbe99f 100644 --- a/apis/v1/groupversion_info.go +++ b/apis/v1/groupversion_info.go @@ -21,5 +21,5 @@ var ( ) func init() { - SchemeBuilder.Register(&ObjectBucket{}, &XObjectBucket{}) + SchemeBuilder.Register(&ObjectBucket{}, &XObjectBucket{}, &VPA{}, &VPAList{}) } diff --git a/apis/v1/vpa_types.go b/apis/v1/vpa_types.go new file mode 100644 index 0000000000..5eccfe1c31 --- /dev/null +++ b/apis/v1/vpa_types.go @@ -0,0 +1,45 @@ +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.spec.selector +// +kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.replicas" +// +kubebuilder:printcolumn:name="Ready",type="integer",JSONPath=".status.replicas" + +// VPA is a simple Custom Resource Definition with scale subresource support +type VPA struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec VPASpec `json:"spec"` + Status VPAStatus `json:"status,omitempty"` +} + +// VPASpec defines the desired state of VPA +type VPASpec struct { + // Replicas is the desired number of replicas + // +kubebuilder:default=1 + Replicas int32 `json:"replicas,omitempty"` + // Selector is the label selector for pods (used by scale subresource) + // +optional + Selector string `json:"selector,omitempty"` +} + +// VPAStatus reflects the observed state of VPA +type VPAStatus struct { + // Replicas is the actual number of replicas + Replicas int32 `json:"replicas,omitempty"` +} + +// +kubebuilder:object:root=true + +// VPAList contains a list of VPA +type VPAList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []VPA `json:"items"` +} diff --git a/apis/v1/zz_generated.deepcopy.go b/apis/v1/zz_generated.deepcopy.go index d40ce4971e..ed094e1bad 100644 --- a/apis/v1/zz_generated.deepcopy.go +++ b/apis/v1/zz_generated.deepcopy.go @@ -115,6 +115,95 @@ func (in *ObjectBucketStatus) DeepCopy() *ObjectBucketStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPA) DeepCopyInto(out *VPA) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPA. +func (in *VPA) DeepCopy() *VPA { + if in == nil { + return nil + } + out := new(VPA) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VPA) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPAList) DeepCopyInto(out *VPAList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VPA, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPAList. +func (in *VPAList) DeepCopy() *VPAList { + if in == nil { + return nil + } + out := new(VPAList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VPAList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPASpec) DeepCopyInto(out *VPASpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPASpec. +func (in *VPASpec) DeepCopy() *VPASpec { + if in == nil { + return nil + } + out := new(VPASpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPAStatus) DeepCopyInto(out *VPAStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPAStatus. +func (in *VPAStatus) DeepCopy() *VPAStatus { + if in == nil { + return nil + } + out := new(VPAStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *XObjectBucket) DeepCopyInto(out *XObjectBucket) { *out = *in diff --git a/config/controller/cluster-role.yaml b/config/controller/cluster-role.yaml index 43ba98de99..6206be42eb 100644 --- a/config/controller/cluster-role.yaml +++ b/config/controller/cluster-role.yaml @@ -126,18 +126,9 @@ rules: - xvshnpostgresqls/status - xvshnredis - xvshnredis/status - - xobjectbuckets verbs: - get - list - patch - update - watch -- apiGroups: - - apiextensions.crossplane.io - resources: - - compositeresourcedefinitions - verbs: - - get - - list - - watch diff --git a/crds/appcat.vshn.io_vpas.yaml b/crds/appcat.vshn.io_vpas.yaml new file mode 100644 index 0000000000..14f7685142 --- /dev/null +++ b/crds/appcat.vshn.io_vpas.yaml @@ -0,0 +1,78 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.5 + name: vpas.appcat.vshn.io +spec: + group: appcat.vshn.io + names: + kind: VPA + listKind: VPAList + plural: vpas + singular: vpa + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.replicas + name: Replicas + type: integer + - jsonPath: .status.replicas + name: Ready + type: integer + name: v1 + schema: + openAPIV3Schema: + description: VPA is a simple Custom Resource Definition with scale subresource + support + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: VPASpec defines the desired state of VPA + properties: + replicas: + default: 1 + description: Replicas is the desired number of replicas + format: int32 + type: integer + selector: + description: Selector is the label selector for pods (used by scale + subresource) + type: string + type: object + status: + description: VPAStatus reflects the observed state of VPA + properties: + replicas: + description: Replicas is the actual number of replicas + format: int32 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + scale: + labelSelectorPath: .spec.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + status: {} diff --git a/pkg/comp-functions/functions/common/vpa.go b/pkg/comp-functions/functions/common/vpa.go new file mode 100644 index 0000000000..60aa2c6210 --- /dev/null +++ b/pkg/comp-functions/functions/common/vpa.go @@ -0,0 +1,77 @@ +package common + +import ( + "context" + "fmt" + + fnproto "github.com/crossplane/function-sdk-go/proto/v1" + appcatv1 "github.com/vshn/appcat/v4/apis/v1" + "github.com/vshn/appcat/v4/pkg/comp-functions/runtime" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// AddVPAConfig creates a VPA CR and a VerticalPodAutoscaler that references it +func AddVPAConfig[T client.Object](ctx context.Context, obj T, svc *runtime.ServiceRuntime) *fnproto.Result { + log := svc.Log + + err := svc.GetObservedComposite(obj) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("can't get composite: %w", err)) + } + + comp := obj.DeepCopyObject().(Composite) + + log.Info("Creating VPA configuration", "service", comp.GetName()) + + // Create the VPA CR + vpaCR := &appcatv1.VPA{ + ObjectMeta: metav1.ObjectMeta{ + Name: comp.GetName() + "-vpa", + Namespace: comp.GetInstanceNamespace(), + Labels: comp.GetLabels(), + }, + Spec: appcatv1.VPASpec{ + Replicas: int32(comp.GetInstances()), + Selector: fmt.Sprintf("cnpg.io/cluster=%s-cluster", comp.GetName()), + }, + } + + err = svc.SetDesiredKubeObject(vpaCR, comp.GetName()+"-vpa-cr", runtime.KubeOptionAllowDeletion) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("could not set desired VPA CR: %w", err)) + } + + // Create the VerticalPodAutoscaler using unstructured + vpaAutoscaler := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "autoscaling.k8s.io/v1", + "kind": "VerticalPodAutoscaler", + "metadata": map[string]interface{}{ + "name": comp.GetName() + "-vpa-autoscaler", + "namespace": comp.GetInstanceNamespace(), + "labels": comp.GetLabels(), + }, + "spec": map[string]interface{}{ + "targetRef": map[string]interface{}{ + "apiVersion": "appcat.vshn.io/v1", + "kind": "VPA", + "name": comp.GetName() + "-vpa", + }, + "updatePolicy": map[string]interface{}{ + "updateMode": "Auto", + }, + }, + }, + } + + err = svc.SetDesiredKubeObject(vpaAutoscaler, comp.GetName()+"-vpa-autoscaler", runtime.KubeOptionAllowDeletion) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("could not set desired VPA autoscaler: %w", err)) + } + + log.Info("VPA configuration created", "vpaCR", vpaCR.Name, "vpaAutoscaler", vpaAutoscaler.GetName()) + + return runtime.NewNormalResult("VPA and VerticalPodAutoscaler created") +} diff --git a/pkg/comp-functions/functions/vshnpostgrescnpg/register.go b/pkg/comp-functions/functions/vshnpostgrescnpg/register.go index 695251534a..5a4ff3b8a7 100644 --- a/pkg/comp-functions/functions/vshnpostgrescnpg/register.go +++ b/pkg/comp-functions/functions/vshnpostgrescnpg/register.go @@ -48,6 +48,10 @@ func init() { Name: "billing", Execute: AddBilling, }, + { + Name: "vpa", + Execute: common.AddVPAConfig[*vshnv1.VSHNPostgreSQL], + }, }, }) }