From 5d1fa1ec75bb62724fffc70585e06bfe10c2d7ac Mon Sep 17 00:00:00 2001 From: Nicolas Bigler Date: Mon, 26 Aug 2024 13:18:09 +0200 Subject: [PATCH] Add support for SPKS and implement sts resizing Signed-off-by: Nicolas Bigler --- Makefile | 5 +- apis/helm/release/v1beta1/types.go | 2 +- apis/syntools/generate.go | 10 + .../v1alpha1/compositeredisinstance.go | 35 +++ apis/syntools/v1alpha1/compositermariadb.go | 35 +++ apis/syntools/v1alpha1/groupversion_info.go | 30 ++ .../v1alpha1/zz_generated.deepcopy.go | 191 ++++++++++++ .../syntools/v1alpha1/zz_generated.managed.go | 125 ++++++++ .../v1alpha1/zz_generated.managedlist.go | 23 ++ cmd/functions.go | 2 + ...cat.vshn.io_compositemariadbinstances.yaml | 265 +++++++++++++++++ ...ppcat.vshn.io_compositeredisinstances.yaml | 265 +++++++++++++++++ .../functions/spksmariadb/pvcresize.go | 268 +++++++++++++++++ .../functions/spksmariadb/register.go | 17 ++ .../functions/spksmariadb/script/recreate.sh | 50 ++++ .../functions/spksredis/pvcresize.go | 276 ++++++++++++++++++ .../functions/spksredis/register.go | 17 ++ .../functions/spksredis/script/recreate.sh | 50 ++++ pkg/scheme.go | 2 + 19 files changed, 1665 insertions(+), 3 deletions(-) create mode 100644 apis/syntools/generate.go create mode 100644 apis/syntools/v1alpha1/compositeredisinstance.go create mode 100644 apis/syntools/v1alpha1/compositermariadb.go create mode 100644 apis/syntools/v1alpha1/groupversion_info.go create mode 100644 apis/syntools/v1alpha1/zz_generated.deepcopy.go create mode 100644 apis/syntools/v1alpha1/zz_generated.managed.go create mode 100644 apis/syntools/v1alpha1/zz_generated.managedlist.go create mode 100644 crds/vshn.appcat.vshn.io_compositemariadbinstances.yaml create mode 100644 crds/vshn.appcat.vshn.io_compositeredisinstances.yaml create mode 100644 pkg/comp-functions/functions/spksmariadb/pvcresize.go create mode 100644 pkg/comp-functions/functions/spksmariadb/register.go create mode 100644 pkg/comp-functions/functions/spksmariadb/script/recreate.sh create mode 100644 pkg/comp-functions/functions/spksredis/pvcresize.go create mode 100644 pkg/comp-functions/functions/spksredis/register.go create mode 100644 pkg/comp-functions/functions/spksredis/script/recreate.sh diff --git a/Makefile b/Makefile index edcd3e36b..a47e5fedf 100644 --- a/Makefile +++ b/Makefile @@ -72,7 +72,7 @@ generate: export PATH := $(go_bin):$(PATH) generate: get-crds generate-stackgres-crds protobuf-gen ## Generate code with controller-gen and protobuf. go version rm -rf apis/generated - go run sigs.k8s.io/controller-tools/cmd/controller-gen paths="{./apis/v1/..., ./apis/vshn/..., ./apis/exoscale/..., ./apis/apiserver/...}" object crd:crdVersions=v1,allowDangerousTypes=true output:artifacts:config=./apis/generated + go run sigs.k8s.io/controller-tools/cmd/controller-gen paths="{./apis/v1/..., ./apis/vshn/..., ./apis/exoscale/..., ./apis/apiserver/..., ./apis/syntools/...}" object crd:crdVersions=v1,allowDangerousTypes=true output:artifacts:config=./apis/generated go generate ./... # Because yaml is such a fun and easy specification, we need to hack some things here. # Depending on the yaml parser implementation the equal sign (=) has special meaning, or not... @@ -196,7 +196,8 @@ clean: get-crds: ./hack/get_crds.sh https://github.com/crossplane-contrib/provider-helm provider-helm apis/release apis/helm ./hack/get_crds.sh https://github.com/crossplane-contrib/provider-kubernetes provider-kubernetes apis/object/v1alpha2 apis/kubernetes - + # There is currently a bug with the serialization if `inline` and `omitempty` are set: https://github.com/crossplane/function-sdk-go/issues/161 + sed -i 's/inline,omitempty/inline/g' apis/helm/release/v1beta1/types.go # provider-sql needs manual fixes... Running this every time would break them. # The crossplane code generator only works if the code is valid, but the code is not valid until the code generator has run... #./hack/get_crds.sh https://github.com/crossplane-contrib/provider-sql provider-sql apis/ apis/sql diff --git a/apis/helm/release/v1beta1/types.go b/apis/helm/release/v1beta1/types.go index 872128722..7a4035aa1 100644 --- a/apis/helm/release/v1beta1/types.go +++ b/apis/helm/release/v1beta1/types.go @@ -47,7 +47,7 @@ type NamespacedName struct { // DataKeySelector defines required spec to access a key of a configmap or secret type DataKeySelector struct { - NamespacedName `json:",inline,omitempty"` + NamespacedName `json:",inline"` Key string `json:"key,omitempty"` Optional bool `json:"optional,omitempty"` } diff --git a/apis/syntools/generate.go b/apis/syntools/generate.go new file mode 100644 index 000000000..aaad18db5 --- /dev/null +++ b/apis/syntools/generate.go @@ -0,0 +1,10 @@ +//go:build generate + +// Remove existing manifests + +// Generate deepcopy methodsets and CRD manifests + +// Generate crossplane-runtime methodsets (resource.Claim, etc) +//go:generate go run -tags generate github.com/crossplane/crossplane-tools/cmd/angryjet generate-methodsets --header-file=../../.github/boilerplate.go.txt ./... + +package syntools diff --git a/apis/syntools/v1alpha1/compositeredisinstance.go b/apis/syntools/v1alpha1/compositeredisinstance.go new file mode 100644 index 000000000..ff36dc033 --- /dev/null +++ b/apis/syntools/v1alpha1/compositeredisinstance.go @@ -0,0 +1,35 @@ +package v1alpha1 + +import ( + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true + +type CompositeRedisInstance struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec CompositeRedisInstanceSpec `json:"spec"` + Status CompositeRedisInstanceStatus `json:"status,omitempty"` +} + +type CompositeRedisInstanceStatus struct { + xpv1.ResourceStatus `json:",inline"` +} + +type CompositeRedisInstanceSpec struct { + xpv1.ResourceSpec `json:",inline"` +} + +// +kubebuilder:object:generate=true +// +kubebuilder:object:root=true + +// CompositeRedisInstanceList represents a list of composites +type CompositeRedisInstanceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []CompositeRedisInstance `json:"items"` +} diff --git a/apis/syntools/v1alpha1/compositermariadb.go b/apis/syntools/v1alpha1/compositermariadb.go new file mode 100644 index 000000000..b42860461 --- /dev/null +++ b/apis/syntools/v1alpha1/compositermariadb.go @@ -0,0 +1,35 @@ +package v1alpha1 + +import ( + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true + +type CompositeMariaDBInstance struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec CompositeMariaDBInstanceSpec `json:"spec"` + Status CompositeMariaDBInstanceStatus `json:"status,omitempty"` +} + +type CompositeMariaDBInstanceStatus struct { + xpv1.ResourceStatus `json:",inline"` +} + +type CompositeMariaDBInstanceSpec struct { + xpv1.ResourceSpec `json:",inline"` +} + +// +kubebuilder:object:generate=true +// +kubebuilder:object:root=true + +// CompositeMariaDBInstanceList represents a list of composites +type CompositeMariaDBInstanceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []CompositeMariaDBInstance `json:"items"` +} diff --git a/apis/syntools/v1alpha1/groupversion_info.go b/apis/syntools/v1alpha1/groupversion_info.go new file mode 100644 index 000000000..2c804d6d1 --- /dev/null +++ b/apis/syntools/v1alpha1/groupversion_info.go @@ -0,0 +1,30 @@ +// +kubebuilder:object:generate=true +// +groupName=vshn.appcat.vshn.io +// +versionName=v1 + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "syn.tools", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +func init() { + SchemeBuilder.Register( + &CompositeRedisInstance{}, + &CompositeRedisInstanceList{}, + &CompositeMariaDBInstance{}, + &CompositeMariaDBInstanceList{}, + ) +} diff --git a/apis/syntools/v1alpha1/zz_generated.deepcopy.go b/apis/syntools/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..a1dae14e1 --- /dev/null +++ b/apis/syntools/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,191 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CompositeMariaDBInstance) DeepCopyInto(out *CompositeMariaDBInstance) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositeMariaDBInstance. +func (in *CompositeMariaDBInstance) DeepCopy() *CompositeMariaDBInstance { + if in == nil { + return nil + } + out := new(CompositeMariaDBInstance) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CompositeMariaDBInstance) 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 *CompositeMariaDBInstanceList) DeepCopyInto(out *CompositeMariaDBInstanceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CompositeMariaDBInstance, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositeMariaDBInstanceList. +func (in *CompositeMariaDBInstanceList) DeepCopy() *CompositeMariaDBInstanceList { + if in == nil { + return nil + } + out := new(CompositeMariaDBInstanceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CompositeMariaDBInstanceList) 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 *CompositeMariaDBInstanceSpec) DeepCopyInto(out *CompositeMariaDBInstanceSpec) { + *out = *in + in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositeMariaDBInstanceSpec. +func (in *CompositeMariaDBInstanceSpec) DeepCopy() *CompositeMariaDBInstanceSpec { + if in == nil { + return nil + } + out := new(CompositeMariaDBInstanceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CompositeMariaDBInstanceStatus) DeepCopyInto(out *CompositeMariaDBInstanceStatus) { + *out = *in + in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositeMariaDBInstanceStatus. +func (in *CompositeMariaDBInstanceStatus) DeepCopy() *CompositeMariaDBInstanceStatus { + if in == nil { + return nil + } + out := new(CompositeMariaDBInstanceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CompositeRedisInstance) DeepCopyInto(out *CompositeRedisInstance) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositeRedisInstance. +func (in *CompositeRedisInstance) DeepCopy() *CompositeRedisInstance { + if in == nil { + return nil + } + out := new(CompositeRedisInstance) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CompositeRedisInstance) 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 *CompositeRedisInstanceList) DeepCopyInto(out *CompositeRedisInstanceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CompositeRedisInstance, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositeRedisInstanceList. +func (in *CompositeRedisInstanceList) DeepCopy() *CompositeRedisInstanceList { + if in == nil { + return nil + } + out := new(CompositeRedisInstanceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CompositeRedisInstanceList) 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 *CompositeRedisInstanceSpec) DeepCopyInto(out *CompositeRedisInstanceSpec) { + *out = *in + in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositeRedisInstanceSpec. +func (in *CompositeRedisInstanceSpec) DeepCopy() *CompositeRedisInstanceSpec { + if in == nil { + return nil + } + out := new(CompositeRedisInstanceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CompositeRedisInstanceStatus) DeepCopyInto(out *CompositeRedisInstanceStatus) { + *out = *in + in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositeRedisInstanceStatus. +func (in *CompositeRedisInstanceStatus) DeepCopy() *CompositeRedisInstanceStatus { + if in == nil { + return nil + } + out := new(CompositeRedisInstanceStatus) + in.DeepCopyInto(out) + return out +} diff --git a/apis/syntools/v1alpha1/zz_generated.managed.go b/apis/syntools/v1alpha1/zz_generated.managed.go new file mode 100644 index 000000000..f2c23e93f --- /dev/null +++ b/apis/syntools/v1alpha1/zz_generated.managed.go @@ -0,0 +1,125 @@ +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha1 + +import xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + +// GetCondition of this CompositeMariaDBInstance. +func (mg *CompositeMariaDBInstance) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this CompositeMariaDBInstance. +func (mg *CompositeMariaDBInstance) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this CompositeMariaDBInstance. +func (mg *CompositeMariaDBInstance) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this CompositeMariaDBInstance. +func (mg *CompositeMariaDBInstance) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetPublishConnectionDetailsTo of this CompositeMariaDBInstance. +func (mg *CompositeMariaDBInstance) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { + return mg.Spec.PublishConnectionDetailsTo +} + +// GetWriteConnectionSecretToReference of this CompositeMariaDBInstance. +func (mg *CompositeMariaDBInstance) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this CompositeMariaDBInstance. +func (mg *CompositeMariaDBInstance) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this CompositeMariaDBInstance. +func (mg *CompositeMariaDBInstance) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this CompositeMariaDBInstance. +func (mg *CompositeMariaDBInstance) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this CompositeMariaDBInstance. +func (mg *CompositeMariaDBInstance) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetPublishConnectionDetailsTo of this CompositeMariaDBInstance. +func (mg *CompositeMariaDBInstance) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) { + mg.Spec.PublishConnectionDetailsTo = r +} + +// SetWriteConnectionSecretToReference of this CompositeMariaDBInstance. +func (mg *CompositeMariaDBInstance) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} + +// GetCondition of this CompositeRedisInstance. +func (mg *CompositeRedisInstance) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this CompositeRedisInstance. +func (mg *CompositeRedisInstance) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this CompositeRedisInstance. +func (mg *CompositeRedisInstance) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this CompositeRedisInstance. +func (mg *CompositeRedisInstance) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetPublishConnectionDetailsTo of this CompositeRedisInstance. +func (mg *CompositeRedisInstance) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { + return mg.Spec.PublishConnectionDetailsTo +} + +// GetWriteConnectionSecretToReference of this CompositeRedisInstance. +func (mg *CompositeRedisInstance) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this CompositeRedisInstance. +func (mg *CompositeRedisInstance) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this CompositeRedisInstance. +func (mg *CompositeRedisInstance) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this CompositeRedisInstance. +func (mg *CompositeRedisInstance) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this CompositeRedisInstance. +func (mg *CompositeRedisInstance) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetPublishConnectionDetailsTo of this CompositeRedisInstance. +func (mg *CompositeRedisInstance) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) { + mg.Spec.PublishConnectionDetailsTo = r +} + +// SetWriteConnectionSecretToReference of this CompositeRedisInstance. +func (mg *CompositeRedisInstance) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} diff --git a/apis/syntools/v1alpha1/zz_generated.managedlist.go b/apis/syntools/v1alpha1/zz_generated.managedlist.go new file mode 100644 index 000000000..0cf1c0438 --- /dev/null +++ b/apis/syntools/v1alpha1/zz_generated.managedlist.go @@ -0,0 +1,23 @@ +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha1 + +import resource "github.com/crossplane/crossplane-runtime/pkg/resource" + +// GetItems of this CompositeMariaDBInstanceList. +func (l *CompositeMariaDBInstanceList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} + +// GetItems of this CompositeRedisInstanceList. +func (l *CompositeRedisInstanceList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} diff --git a/cmd/functions.go b/cmd/functions.go index f5a970298..6b121b9a8 100644 --- a/cmd/functions.go +++ b/cmd/functions.go @@ -8,6 +8,8 @@ import ( _ "github.com/vshn/appcat/v4/pkg/comp-functions/functions/buckets/cloudscalebucket" _ "github.com/vshn/appcat/v4/pkg/comp-functions/functions/buckets/exoscalebucket" _ "github.com/vshn/appcat/v4/pkg/comp-functions/functions/buckets/miniobucket" + _ "github.com/vshn/appcat/v4/pkg/comp-functions/functions/spksmariadb" + _ "github.com/vshn/appcat/v4/pkg/comp-functions/functions/spksredis" _ "github.com/vshn/appcat/v4/pkg/comp-functions/functions/vshnkeycloak" _ "github.com/vshn/appcat/v4/pkg/comp-functions/functions/vshnmariadb" _ "github.com/vshn/appcat/v4/pkg/comp-functions/functions/vshnminio" diff --git a/crds/vshn.appcat.vshn.io_compositemariadbinstances.yaml b/crds/vshn.appcat.vshn.io_compositemariadbinstances.yaml new file mode 100644 index 000000000..718a5f889 --- /dev/null +++ b/crds/vshn.appcat.vshn.io_compositemariadbinstances.yaml @@ -0,0 +1,265 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: compositemariadbinstances.vshn.appcat.vshn.io +spec: + group: vshn.appcat.vshn.io + names: + kind: CompositeMariaDBInstance + listKind: CompositeMariaDBInstanceList + plural: compositemariadbinstances + singular: compositemariadbinstance + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + 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: + properties: + deletionPolicy: + default: Delete + description: |- + DeletionPolicy specifies what will happen to the underlying external + when this managed resource is deleted - either "Delete" or "Orphan" the + external resource. + This field is planned to be deprecated in favor of the ManagementPolicies + field in a future release. Currently, both could be set independently and + non-default values would be honored if the feature flag is enabled. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + enum: + - Orphan + - Delete + type: string + managementPolicies: + default: + - '*' + description: |- + THIS IS A BETA FIELD. It is on by default but can be opted out + through a Crossplane feature flag. + ManagementPolicies specify the array of actions Crossplane is allowed to + take on the managed and external resources. + This field is planned to replace the DeletionPolicy field in a future + release. Currently, both could be set independently and non-default + values would be honored if the feature flag is enabled. If both are + custom, the DeletionPolicy field will be ignored. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md + items: + description: |- + A ManagementAction represents an action that the Crossplane controllers + can take on an external resource. + enum: + - Observe + - Create + - Update + - Delete + - LateInitialize + - '*' + type: string + type: array + providerConfigRef: + default: + name: default + description: |- + ProviderConfigReference specifies how the provider that will be used to + create, observe, update, and delete this managed resource should be + configured. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + publishConnectionDetailsTo: + description: |- + PublishConnectionDetailsTo specifies the connection secret config which + contains a name, metadata and a reference to secret store config to + which any connection details for this managed resource should be written. + Connection details frequently include the endpoint, username, + and password required to connect to the managed resource. + properties: + configRef: + default: + name: default + description: |- + SecretStoreConfigRef specifies which secret store config should be used + for this ConnectionSecret. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + metadata: + description: Metadata is the metadata for connection secret. + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations are the annotations to be added to connection secret. + - For Kubernetes secrets, this will be used as "metadata.annotations". + - It is up to Secret Store implementation for others store types. + type: object + labels: + additionalProperties: + type: string + description: |- + Labels are the labels/tags to be added to connection secret. + - For Kubernetes secrets, this will be used as "metadata.labels". + - It is up to Secret Store implementation for others store types. + type: object + type: + description: |- + Type is the SecretType for the connection secret. + - Only valid for Kubernetes Secret Stores. + type: string + type: object + name: + description: Name is the name of the connection secret. + type: string + required: + - name + type: object + writeConnectionSecretToRef: + description: |- + WriteConnectionSecretToReference specifies the namespace and name of a + Secret to which any connection details for this managed resource should + be written. Connection details frequently include the endpoint, username, + and password required to connect to the managed resource. + This field is planned to be replaced in a future release in favor of + PublishConnectionDetailsTo. Currently, both could be set independently + and connection details would be published to both without affecting + each other. + properties: + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - name + - namespace + type: object + type: object + status: + properties: + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastTransitionTime: + description: |- + LastTransitionTime is the last time this condition transitioned from one + status to another. + format: date-time + type: string + message: + description: |- + A Message containing details about this condition's last transition from + one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown? + type: string + type: + description: |- + Type of this condition. At most one of each condition type may apply to + a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true diff --git a/crds/vshn.appcat.vshn.io_compositeredisinstances.yaml b/crds/vshn.appcat.vshn.io_compositeredisinstances.yaml new file mode 100644 index 000000000..280bfa837 --- /dev/null +++ b/crds/vshn.appcat.vshn.io_compositeredisinstances.yaml @@ -0,0 +1,265 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: compositeredisinstances.vshn.appcat.vshn.io +spec: + group: vshn.appcat.vshn.io + names: + kind: CompositeRedisInstance + listKind: CompositeRedisInstanceList + plural: compositeredisinstances + singular: compositeredisinstance + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + 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: + properties: + deletionPolicy: + default: Delete + description: |- + DeletionPolicy specifies what will happen to the underlying external + when this managed resource is deleted - either "Delete" or "Orphan" the + external resource. + This field is planned to be deprecated in favor of the ManagementPolicies + field in a future release. Currently, both could be set independently and + non-default values would be honored if the feature flag is enabled. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + enum: + - Orphan + - Delete + type: string + managementPolicies: + default: + - '*' + description: |- + THIS IS A BETA FIELD. It is on by default but can be opted out + through a Crossplane feature flag. + ManagementPolicies specify the array of actions Crossplane is allowed to + take on the managed and external resources. + This field is planned to replace the DeletionPolicy field in a future + release. Currently, both could be set independently and non-default + values would be honored if the feature flag is enabled. If both are + custom, the DeletionPolicy field will be ignored. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md + items: + description: |- + A ManagementAction represents an action that the Crossplane controllers + can take on an external resource. + enum: + - Observe + - Create + - Update + - Delete + - LateInitialize + - '*' + type: string + type: array + providerConfigRef: + default: + name: default + description: |- + ProviderConfigReference specifies how the provider that will be used to + create, observe, update, and delete this managed resource should be + configured. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + publishConnectionDetailsTo: + description: |- + PublishConnectionDetailsTo specifies the connection secret config which + contains a name, metadata and a reference to secret store config to + which any connection details for this managed resource should be written. + Connection details frequently include the endpoint, username, + and password required to connect to the managed resource. + properties: + configRef: + default: + name: default + description: |- + SecretStoreConfigRef specifies which secret store config should be used + for this ConnectionSecret. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + metadata: + description: Metadata is the metadata for connection secret. + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations are the annotations to be added to connection secret. + - For Kubernetes secrets, this will be used as "metadata.annotations". + - It is up to Secret Store implementation for others store types. + type: object + labels: + additionalProperties: + type: string + description: |- + Labels are the labels/tags to be added to connection secret. + - For Kubernetes secrets, this will be used as "metadata.labels". + - It is up to Secret Store implementation for others store types. + type: object + type: + description: |- + Type is the SecretType for the connection secret. + - Only valid for Kubernetes Secret Stores. + type: string + type: object + name: + description: Name is the name of the connection secret. + type: string + required: + - name + type: object + writeConnectionSecretToRef: + description: |- + WriteConnectionSecretToReference specifies the namespace and name of a + Secret to which any connection details for this managed resource should + be written. Connection details frequently include the endpoint, username, + and password required to connect to the managed resource. + This field is planned to be replaced in a future release in favor of + PublishConnectionDetailsTo. Currently, both could be set independently + and connection details would be published to both without affecting + each other. + properties: + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - name + - namespace + type: object + type: object + status: + properties: + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastTransitionTime: + description: |- + LastTransitionTime is the last time this condition transitioned from one + status to another. + format: date-time + type: string + message: + description: |- + A Message containing details about this condition's last transition from + one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown? + type: string + type: + description: |- + Type of this condition. At most one of each condition type may apply to + a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true diff --git a/pkg/comp-functions/functions/spksmariadb/pvcresize.go b/pkg/comp-functions/functions/spksmariadb/pvcresize.go new file mode 100644 index 000000000..1dd783844 --- /dev/null +++ b/pkg/comp-functions/functions/spksmariadb/pvcresize.go @@ -0,0 +1,268 @@ +package spksmariadb + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + xfnproto "github.com/crossplane/function-sdk-go/proto/v1beta1" + helmv1beta1 "github.com/vshn/appcat/v4/apis/helm/release/v1beta1" + xkubev1 "github.com/vshn/appcat/v4/apis/kubernetes/v1alpha2" + spksv1alpha1 "github.com/vshn/appcat/v4/apis/syntools/v1alpha1" + "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common" + "github.com/vshn/appcat/v4/pkg/comp-functions/runtime" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +var mariadbRelease = "mariadb-chart" + +//go:embed script/recreate.sh +var stsRecreateScript string + +// ResizeSpksPVCs will add a job to do the pvc resize for the instance +func ResizeSpksPVCs(ctx context.Context, comp *spksv1alpha1.CompositeMariaDBInstance, svc *runtime.ServiceRuntime) *xfnproto.Result { + + err := svc.GetObservedComposite(comp) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("failed to parse composite: %w", err)) + } + + release := &helmv1beta1.Release{} + err = svc.GetDesiredComposedResourceByName(release, mariadbRelease) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot mariadb release from desired iof: %w", err)) + } + + values, err := common.GetReleaseValues(release) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot parse release values from desired release: %w", err)) + } + + if val, ok := release.GetAnnotations()["crossplane.io/paused"]; ok && val == "true" { + // The release has just been updated and paused and is waiting for the deletion job to finish + // The deletion job should remove the annotation once it's done. + + xJob := &xkubev1.Object{} + err = svc.GetObservedComposedResource(xJob, comp.Name+"-sts-deleter") + if err != nil && err != runtime.ErrNotFound { + return runtime.NewFatalResult(fmt.Errorf("cannot get observed deletion job: %w", err)) + } + // If there's no job observed, we're done here. + if err == runtime.ErrNotFound { + return runtime.NewFatalResult(fmt.Errorf("cannot get observed deletion job, but release is paused: %w", err)) + } + + sts := &appsv1.StatefulSet{} + err = svc.GetObservedKubeObject(sts, comp.Name+"-sts-observer") + if err != nil && err != runtime.ErrNotFound { + return runtime.NewFatalResult(fmt.Errorf("cannot get observed statefulset job: %w", err)) + } + + // If the xkube object has been created it's still possible that the actual job hasn't been observedJob. + observedJob := len(xJob.Status.AtProvider.Manifest.Raw) > 0 + + // Check the sts if it has been updated + stsSize := int64(0) + if len(sts.Spec.VolumeClaimTemplates) > 0 { + stsSize, _ = sts.Spec.VolumeClaimTemplates[0].Spec.Resources.Requests.Storage().AsInt64() + } + newSize, found, err := unstructured.NestedString(values, "persistence", "size") + if !found { + return runtime.NewFatalResult(fmt.Errorf("disk size not found in observed release")) + } + + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("failed to read size from observed release: %w", err)) + } + desiredSize, err := getSizeAsInt(newSize) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot parse desired size: %w", err)) + } + stsUpdated := stsSize == desiredSize + + deletionJob := &batchv1.Job{} + if observedJob { + err := json.Unmarshal(xJob.Status.AtProvider.Manifest.Raw, deletionJob) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot unmarshal sts deleter job: %w", err)) + } + } + + // The job hasn't been observed yet, so we need to keep it in desired, or we will have a recreate loop + // Also as long as it hasn't finished we need to make sure it exists. + if (!observedJob || deletionJob.Status.Succeeded < 1) || (sts.Status.ReadyReplicas == 0 && !stsUpdated) { + err := addDeletionJob(svc, comp, newSize, release.GetName()) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot create RBAC for the deletion job: %w", err)) + } + } + return nil + } + + err = addStsObserver(svc, comp) + if err != nil { + return runtime.NewWarningResult(fmt.Errorf("cannot observe sts: %w", err).Error()) + } + + sts := &appsv1.StatefulSet{} + err = svc.GetObservedKubeObject(sts, comp.Name+"-sts-observer") + if err == runtime.ErrNotFound { + return nil + } + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot get observed statefulset: %w", err)) + } + stsSize := int64(0) + // Check the current size in the sts + if len(sts.Spec.VolumeClaimTemplates) > 0 { + stsSize, _ = sts.Spec.VolumeClaimTemplates[0].Spec.Resources.Requests.Storage().AsInt64() + } + + patch, newSize, result := needReleasePatch(values, stsSize) + if result != nil { + return result + } + + if patch { + err = addDeletionJob(svc, comp, newSize, release.GetName()) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot create the deletion job: %w", err)) + } + // We pause the release at this point to make sure that provider-helm doesn't update the + // release until the deletion job removed the sts + release.SetAnnotations(map[string]string{ + "crossplane.io/paused": "true", + }) + + err = svc.SetDesiredComposedResourceWithName(release, mariadbRelease) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("Can't pause the release: %w", err)) + } + return nil + } + + return nil +} + +func addStsObserver(svc *runtime.ServiceRuntime, comp *spksv1alpha1.CompositeMariaDBInstance) error { + statefulset := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mariadb", + Namespace: comp.GetName(), + }, + } + + providerConfigRef := comp.GetLabels()["service.syn.tools/cluster"] + + if providerConfigRef == "" { + return fmt.Errorf("Can't get cluster from label") + } + + return svc.SetDesiredKubeObject(statefulset, comp.GetName()+"-sts-observer", KubeOptionAddProviderRef(providerConfigRef, true)) +} + +func needReleasePatch(values map[string]interface{}, stsSize int64) (bool, string, *xfnproto.Result) { + releaseSizeValue, found, err := unstructured.NestedString(values, "persistence", "size") + if !found { + return false, "", runtime.NewFatalResult(fmt.Errorf("disk size not found in observed release")) + } + + if err != nil { + return false, "", runtime.NewFatalResult(fmt.Errorf("failed to read size from observed release: %w", err)) + } + + releaseInt, err := getSizeAsInt(releaseSizeValue) + if err != nil { + return false, "", runtime.NewFatalResult(fmt.Errorf("cannot parse release disk size from observed releaes: %w", err)) + } + + return releaseInt > stsSize, releaseSizeValue, nil + +} + +func getSizeAsInt(size string) (int64, error) { + parsedSize, err := resource.ParseQuantity(size) + if err != nil { + return 0, err + } + finalSize, _ := parsedSize.AsInt64() + return finalSize, nil +} + +func addDeletionJob(svc *runtime.ServiceRuntime, comp *spksv1alpha1.CompositeMariaDBInstance, newSize string, releaseName string) error { + ns := svc.Config.Data["spksNamespace"] + stsDeleterImage := svc.Config.Data["stsDeleterImage"] + + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: comp.Name + "sts-deletion-job", + Namespace: ns, + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + ServiceAccountName: "sa-sts-deleter", + RestartPolicy: corev1.RestartPolicyNever, + Containers: []corev1.Container{ + { + Name: "sts-deleter", + Image: stsDeleterImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Env: []corev1.EnvVar{ + { + Name: "STS_NAME", + Value: "mariadb", + }, + { + Name: "STS_NAMESPACE", + Value: comp.GetName(), + }, + { + Name: "STS_SIZE", + Value: newSize, + }, + { + Name: "RELEASE_NAME", + Value: releaseName, + }, + }, + Command: []string{ + "bash", + "-c", + }, + Args: []string{ + stsRecreateScript, + }, + }, + }, + }, + }, + }, + } + + providerConfigRef := comp.GetLabels()["service.syn.tools/cluster"] + + if providerConfigRef == "" { + return fmt.Errorf("Can't get cluster from label") + } + + return svc.SetDesiredKubeObject(job, comp.Name+"-sts-deleter", KubeOptionAddProviderRef(providerConfigRef, false)) +} + +// KubeOptionAddProviderRef adds the given correct ProviderConfigRef to the kube object. +func KubeOptionAddProviderRef(providerConfigName string, observer bool) runtime.KubeObjectOption { + return func(obj *xkubev1.Object) { + obj.Spec.ProviderConfigReference.Name = providerConfigName + if observer { + obj.Spec.ManagementPolicies = nil + obj.Spec.ManagementPolicies = append(obj.Spec.ManagementPolicies, xpv1.ManagementActionObserve) + } + } +} diff --git a/pkg/comp-functions/functions/spksmariadb/register.go b/pkg/comp-functions/functions/spksmariadb/register.go new file mode 100644 index 000000000..91d6f0755 --- /dev/null +++ b/pkg/comp-functions/functions/spksmariadb/register.go @@ -0,0 +1,17 @@ +package spksmariadb + +import ( + "github.com/vshn/appcat/v4/apis/syntools/v1alpha1" + "github.com/vshn/appcat/v4/pkg/comp-functions/runtime" +) + +func init() { + runtime.RegisterService("mariadb-k8s", runtime.Service[*v1alpha1.CompositeMariaDBInstance]{ + Steps: []runtime.Step[*v1alpha1.CompositeMariaDBInstance]{ + { + Name: "resizePVC", + Execute: ResizeSpksPVCs, + }, + }, + }) +} diff --git a/pkg/comp-functions/functions/spksmariadb/script/recreate.sh b/pkg/comp-functions/functions/spksmariadb/script/recreate.sh new file mode 100644 index 000000000..fc5485a6d --- /dev/null +++ b/pkg/comp-functions/functions/spksmariadb/script/recreate.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +set -eox pipefail + +name="$STS_NAME" +namespace="$STS_NAMESPACE" +size="$STS_SIZE" +release="$RELEASE_NAME" + +echo "Checking if the PVC sizes match" +# Check if delete is necessary +found=$(kubectl -n "$namespace" get sts "$name" -o json --ignore-not-found) + +foundsize=$(echo -En "$found" | jq -r '.spec.volumeClaimTemplates[] | select(.metadata.name=="data") | .spec.resources.requests.storage') + +paused=$(kubectl get release "$release" -o jsonpath='{.metadata.annotations.crossplane\.io\/paused}' --ignore-not-found) + +i=0 +while [[ i -lt 300 ]]; do + if [[ $foundsize != "$size" ]]; then + echo "PVC sizes don't match, deleting sts" + # We try to delete the sts and wait for 5s. On APPUiO it can happen that the + # deletion with orphan doesn't go through and the sts is stuck with an orphan finalizer. + # So if the delete hasn't returned after 5s we forcefully patch away the finalizer. + kubectl -n "$namespace" delete sts "$name" --cascade=orphan --ignore-not-found --wait=true --timeout 5s || true + kubectl -n "$namespace" patch sts "$name" -p '{"metadata":{"finalizers":null}}' || true + # Upause the release so that the sts is recreated. We pause the release to avoid provider-helm updating the release + # before the sts is deleted. + # Then we first patch the siye to garbage and afterwards to the right size to enforce an upgrade + echo "Triggering sts re-creation" + kubectl annotate release "$release" "crossplane.io/paused-" + kubectl patch release "$release" --type merge -p "{\"spec\":{\"forProvider\":{\"values\":{\"persistence\":{\"size\":\"foo\"}}}}}" + kubectl patch release "$release" --type merge -p "{\"spec\":{\"forProvider\":{\"values\":{\"persistence\":{\"size\":\"$size\"}}}}}" + count=0 + while ! kubectl -n "$namespace" get sts "$name" && [[ count -lt 300 ]]; do + echo "waiting for sts to re-appear" + count=$count+1 + sleep 1 + done + [[ $count -lt 300 ]] || (echo "Waited for 5 minutes for sts to re-appear"; exit 1) + echo "Set label on sts to trigger the statefulset-resize-controller" + kubectl -n "$namespace" label sts "$name" --overwrite "sts-resize.vshn.net/resize-inplace=true" + break + else + echo "Sizes match, nothing to do" + fi + i=$i+1 + sleep 1 +done +trap "kubectl annotate release \"$release\" \"crossplane.io/paused-\"" EXIT diff --git a/pkg/comp-functions/functions/spksredis/pvcresize.go b/pkg/comp-functions/functions/spksredis/pvcresize.go new file mode 100644 index 000000000..6db08f33e --- /dev/null +++ b/pkg/comp-functions/functions/spksredis/pvcresize.go @@ -0,0 +1,276 @@ +package spksredis + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + xfnproto "github.com/crossplane/function-sdk-go/proto/v1beta1" + helmv1beta1 "github.com/vshn/appcat/v4/apis/helm/release/v1beta1" + xkubev1 "github.com/vshn/appcat/v4/apis/kubernetes/v1alpha2" + spksv1alpha1 "github.com/vshn/appcat/v4/apis/syntools/v1alpha1" + "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common" + "github.com/vshn/appcat/v4/pkg/comp-functions/runtime" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +var redisRelease = "redis-chart" + +//go:embed script/recreate.sh +var stsRecreateScript string + +// ResizeSpksPVCs will add a job to do the pvc resize for the instance +func ResizeSpksPVCs(ctx context.Context, comp *spksv1alpha1.CompositeRedisInstance, svc *runtime.ServiceRuntime) *xfnproto.Result { + + replicaKey := svc.Config.Data["replicaKey"] + if replicaKey == "" { + return runtime.NewFatalResult(fmt.Errorf("failed to parse replicaKey config value")) + } + err := svc.GetObservedComposite(comp) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("failed to parse composite: %w", err)) + } + + release := &helmv1beta1.Release{} + err = svc.GetDesiredComposedResourceByName(release, redisRelease) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot get redis release from desired iof: %v", err)) + } + + values, err := common.GetReleaseValues(release) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot parse release values from desired release: %w", err)) + } + + if val, ok := release.GetAnnotations()["crossplane.io/paused"]; ok && val == "true" { + // The release has just been updated and paused and is waiting for the deletion job to finish + // The deletion job should remove the annotation once it's done. + + xJob := &xkubev1.Object{} + err = svc.GetObservedComposedResource(xJob, comp.Name+"-sts-deleter") + if err != nil && err != runtime.ErrNotFound { + return runtime.NewFatalResult(fmt.Errorf("cannot get observed deletion job: %w", err)) + } + // If there's no job observed, we're done here. + if err == runtime.ErrNotFound { + return runtime.NewFatalResult(fmt.Errorf("cannot get observed deletion job, but release is paused: %w", err)) + } + + sts := &appsv1.StatefulSet{} + err = svc.GetObservedKubeObject(sts, comp.GetName()+"-sts-observer") + if err != nil && err != runtime.ErrNotFound { + return runtime.NewFatalResult(fmt.Errorf("cannot get observed statefulset job: %w", err)) + } + + // If the xkube object has been created it's still possible that the actual job hasn't been observedJob. + observedJob := len(xJob.Status.AtProvider.Manifest.Raw) > 0 + + // Check the sts if it has been updated + stsSize := int64(0) + if len(sts.Spec.VolumeClaimTemplates) > 0 { + stsSize, _ = sts.Spec.VolumeClaimTemplates[0].Spec.Resources.Requests.Storage().AsInt64() + } + newSize, found, err := unstructured.NestedString(values, replicaKey, "persistence", "size") + if !found { + return runtime.NewFatalResult(fmt.Errorf("disk size not found in observed release")) + } + + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("failed to read size from observed release: %w", err)) + } + desiredSize, err := getSizeAsInt(newSize) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot parse desired size: %w", err)) + } + stsUpdated := stsSize == desiredSize + + deletionJob := &batchv1.Job{} + if observedJob { + err := json.Unmarshal(xJob.Status.AtProvider.Manifest.Raw, deletionJob) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot unmarshal sts deleter job: %w", err)) + } + } + + // The job hasn't been observed yet, so we need to keep it in desired, or we will have a recreate loop + // Also as long as it hasn't finished we need to make sure it exists. + if (!observedJob || deletionJob.Status.Succeeded < 1) || (sts.Status.ReadyReplicas == 0 && !stsUpdated) { + err := addDeletionJob(svc, comp, newSize, release.GetName(), replicaKey) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot create RBAC for the deletion job: %w", err)) + } + } + return nil + } + + err = addStsObserver(svc, comp) + if err != nil { + return runtime.NewWarningResult(fmt.Errorf("cannot observe sts: %w", err).Error()) + } + + sts := &appsv1.StatefulSet{} + err = svc.GetObservedKubeObject(sts, comp.GetName()+"-sts-observer") + if err == runtime.ErrNotFound { + return nil + } + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot get observed statefulset: %w", err)) + } + + stsSize := int64(0) + // Check the current size in the sts + if len(sts.Spec.VolumeClaimTemplates) > 0 { + stsSize, _ = sts.Spec.VolumeClaimTemplates[0].Spec.Resources.Requests.Storage().AsInt64() + } + + patch, newSize, result := needReleasePatch(values, stsSize, replicaKey) + if result != nil { + return result + } + + if patch { + err = addDeletionJob(svc, comp, newSize, release.GetName(), replicaKey) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot create the deletion job: %w", err)) + } + // We pause the release at this point to make sure that provider-helm doesn't update the + // release until the deletion job removed the sts + release.SetAnnotations(map[string]string{ + "crossplane.io/paused": "true", + }) + err = svc.SetDesiredComposedResourceWithName(release, redisRelease) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("Can't pause the release: %w", err)) + } + return nil + } + + return nil +} + +func addStsObserver(svc *runtime.ServiceRuntime, comp *spksv1alpha1.CompositeRedisInstance) error { + statefulset := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "redis-node", + Namespace: comp.GetName(), + }, + } + + providerConfigRef := comp.GetLabels()["service.syn.tools/cluster"] + + if providerConfigRef == "" { + return fmt.Errorf("Can't get cluster from label") + } + + return svc.SetDesiredKubeObject(statefulset, comp.GetName()+"-sts-observer", KubeOptionAddProviderRef(providerConfigRef, true)) +} + +func needReleasePatch(values map[string]interface{}, stsSize int64, replicaKey string) (bool, string, *xfnproto.Result) { + releaseSizeValue, found, err := unstructured.NestedString(values, replicaKey, "persistence", "size") + if !found { + return false, "", runtime.NewFatalResult(fmt.Errorf("disk size not found in observed release")) + } + + if err != nil { + return false, "", runtime.NewFatalResult(fmt.Errorf("failed to read size from observed release: %w", err)) + } + + releaseInt, err := getSizeAsInt(releaseSizeValue) + if err != nil { + return false, "", runtime.NewFatalResult(fmt.Errorf("cannot parse release disk size from observed releaes: %w", err)) + } + + return releaseInt > stsSize, releaseSizeValue, nil + +} + +func getSizeAsInt(size string) (int64, error) { + parsedSize, err := resource.ParseQuantity(size) + if err != nil { + return 0, err + } + finalSize, _ := parsedSize.AsInt64() + return finalSize, nil +} + +func addDeletionJob(svc *runtime.ServiceRuntime, comp *spksv1alpha1.CompositeRedisInstance, newSize string, releaseName string, replicaKey string) error { + ns := svc.Config.Data["spksNamespace"] + stsDeleterImage := svc.Config.Data["stsDeleterImage"] + + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: comp.Name + "sts-deletion-job", + Namespace: ns, + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + ServiceAccountName: "sa-sts-deleter", + RestartPolicy: corev1.RestartPolicyNever, + Containers: []corev1.Container{ + { + Name: "sts-deleter", + Image: stsDeleterImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Env: []corev1.EnvVar{ + { + Name: "STS_NAME", + Value: "redis-node", + }, + { + Name: "STS_NAMESPACE", + Value: comp.GetName(), + }, + { + Name: "STS_SIZE", + Value: newSize, + }, + { + Name: "RELEASE_NAME", + Value: releaseName, + }, + { + Name: "REPLICA_KEY", + Value: replicaKey, + }, + }, + Command: []string{ + "bash", + "-c", + }, + Args: []string{ + stsRecreateScript, + }, + }, + }, + }, + }, + }, + } + + providerConfigRef := comp.GetLabels()["service.syn.tools/cluster"] + + if providerConfigRef == "" { + return fmt.Errorf("Can't get cluster from label") + } + + return svc.SetDesiredKubeObject(job, comp.Name+"-sts-deleter", KubeOptionAddProviderRef(providerConfigRef, false)) +} + +// KubeOptionAddProviderRef adds the given correct ProviderConfigRef to the kube object. +func KubeOptionAddProviderRef(providerConfigName string, observer bool) runtime.KubeObjectOption { + return func(obj *xkubev1.Object) { + obj.Spec.ProviderConfigReference.Name = providerConfigName + if observer { + obj.Spec.ManagementPolicies = nil + obj.Spec.ManagementPolicies = append(obj.Spec.ManagementPolicies, xpv1.ManagementActionObserve) + } + } +} diff --git a/pkg/comp-functions/functions/spksredis/register.go b/pkg/comp-functions/functions/spksredis/register.go new file mode 100644 index 000000000..0a77ebc11 --- /dev/null +++ b/pkg/comp-functions/functions/spksredis/register.go @@ -0,0 +1,17 @@ +package spksredis + +import ( + "github.com/vshn/appcat/v4/apis/syntools/v1alpha1" + "github.com/vshn/appcat/v4/pkg/comp-functions/runtime" +) + +func init() { + runtime.RegisterService("redis-k8s", runtime.Service[*v1alpha1.CompositeRedisInstance]{ + Steps: []runtime.Step[*v1alpha1.CompositeRedisInstance]{ + { + Name: "resizePVC", + Execute: ResizeSpksPVCs, + }, + }, + }) +} diff --git a/pkg/comp-functions/functions/spksredis/script/recreate.sh b/pkg/comp-functions/functions/spksredis/script/recreate.sh new file mode 100644 index 000000000..fb1e20c42 --- /dev/null +++ b/pkg/comp-functions/functions/spksredis/script/recreate.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +set -eox pipefail + +name="$STS_NAME" +namespace="$STS_NAMESPACE" +size="$STS_SIZE" +release="$RELEASE_NAME" +replica_key="$REPLICA_KEY" + +echo "Checking if the PVC sizes match" +# Check if delete is necessary +found=$(kubectl -n "$namespace" get sts "$name" -o json --ignore-not-found) + +foundsize=$(echo -En "$found" | jq -r '.spec.volumeClaimTemplates[] | select(.metadata.name=="redis-data") | .spec.resources.requests.storage') + +paused=$(kubectl get release "$release" -o jsonpath='{.metadata.annotations.crossplane\.io\/paused}' --ignore-not-found) + +i=0 +while [[ i -lt 300 ]]; do + if [[ $foundsize != "$size" && $paused == "true" ]]; then + echo "PVC sizes don't match, deleting sts" + # We try to delete the sts and wait for 5s. On APPUiO it can happen that the + # deletion with orphan doesn't go through and the sts is stuck with an orphan finalizer. + # So if the delete hasn't returned after 5s we forcefully patch away the finalizer. + kubectl -n "$namespace" delete sts "$name" --cascade=orphan --ignore-not-found --wait=true --timeout 5s || true + kubectl -n "$namespace" patch sts "$name" -p '{"metadata":{"finalizers":null}}' || true + # Upause the release so that the sts is recreated. We pause the release to avoid provider-helm updating the release + # before the sts is deleted. + echo "Triggering sts re-creation" + kubectl annotate release $release "crossplane.io/paused-" + kubectl patch release "$release" --type merge -p "{\"spec\":{\"forProvider\":{\"values\":{\"$replica_key\":{\"persistence\":{\"size\":\"foo\"}}}}}}" + kubectl patch release "$release" --type merge -p "{\"spec\":{\"forProvider\":{\"values\":{\"$replica_key\":{\"persistence\":{\"size\":\"$size\"}}}}}}" + count=0 + while ! kubectl -n "$namespace" get sts "$name" && [[ count -lt 300 ]]; do + echo "waiting for sts to re-appear" + count=$count+1 + sleep 1 + done + [[ $count -lt 300 ]] || (echo "Waited for 5 minutes for sts to re-appear"; exit 1) + echo "Set label on sts to trigger the statefulset-resize-controller" + kubectl -n "$namespace" label sts "$name" --overwrite "sts-resize.vshn.net/resize-inplace=true" + break + else + echo "Sizes match, nothing to do" + fi + i=$i+1 + sleep 1 +done +trap "kubectl annotate release \"$release\" \"crossplane.io/paused-\"" EXIT diff --git a/pkg/scheme.go b/pkg/scheme.go index 36ee1033d..8fc92c000 100644 --- a/pkg/scheme.go +++ b/pkg/scheme.go @@ -14,6 +14,7 @@ import ( pgv1alpha1 "github.com/vshn/appcat/v4/apis/sql/postgresql/v1alpha1" stackgresv1 "github.com/vshn/appcat/v4/apis/stackgres/v1" stackgresv1beta1 "github.com/vshn/appcat/v4/apis/stackgres/v1beta1" + spksv1alpha1 "github.com/vshn/appcat/v4/apis/syntools/v1alpha1" appcatv1 "github.com/vshn/appcat/v4/apis/v1" vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" miniov1 "github.com/vshn/provider-minio/apis/minio/v1" @@ -60,4 +61,5 @@ func AddToScheme(s *runtime.Scheme) { _ = pdbv1.AddToScheme(s) _ = cloudscalev1.SchemeBuilder.AddToScheme(s) _ = exoscalev1.SchemeBuilder.AddToScheme(s) + _ = spksv1alpha1.SchemeBuilder.AddToScheme(s) }