diff --git a/apis/appcat/v1/appcat_types.go b/apis/appcat/v1/appcat_types.go index d1a6397..92d8945 100644 --- a/apis/appcat/v1/appcat_types.go +++ b/apis/appcat/v1/appcat_types.go @@ -18,7 +18,7 @@ import ( // +kubebuilder:rbac:groups="apiextensions.crossplane.io",resources=compositions,verbs=get;list;watch // +kubebuilder:rbac:groups="stackgres.io",resources=sgbackups,verbs=get;list;watch // +kubebuilder:rbac:groups="k8up.io",resources=snapshots,verbs=get;list;watch -// +kubebuilder:rbac:groups="vshn.appcat.vshn.io",resources=vshnredis;xvshnpostgresqls,verbs=get;list;watch +// +kubebuilder:rbac:groups="vshn.appcat.vshn.io",resources=vshnredis;xvshnpostgresqls;vshnmariadbs;,verbs=get;list;watch var ( // OfferedValue is the label value to identify AppCat services diff --git a/apis/appcat/v1/register.go b/apis/appcat/v1/register.go index 0dabe4c..3c4ce0c 100644 --- a/apis/appcat/v1/register.go +++ b/apis/appcat/v1/register.go @@ -31,6 +31,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &VSHNPostgresBackupList{}, &VSHNRedisBackup{}, &VSHNRedisBackupList{}, + &VSHNMariaDBBackup{}, + &VSHNMariaDBBackupList{}, ) metav1.AddToGroupVersion(scheme, GroupVersion) return nil diff --git a/apis/appcat/v1/vshn_mariadb_backup_types.go b/apis/appcat/v1/vshn_mariadb_backup_types.go new file mode 100644 index 0000000..abd4a3e --- /dev/null +++ b/apis/appcat/v1/vshn_mariadb_backup_types.go @@ -0,0 +1,72 @@ +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/apiserver-runtime/pkg/builder/resource" +) + +// VSHNMariaDBBackup needs to implement the builder resource interface +var _ resource.Object = &VSHNMariaDBBackup{} +var _ resource.ObjectList = &VSHNMariaDBBackupList{} + +// +kubebuilder:object:root=true + +type VSHNMariaDBBackup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Status VSHNMariaDBBackupStatus `json:"status,omitempty"` +} + +type VSHNMariaDBBackupStatus struct { + ID string `json:"id,omitempty"` + Date metav1.Time `json:"date,omitempty"` + Instance string `json:"instance,omitempty"` +} + +// +kubebuilder:object:root=true + +type VSHNMariaDBBackupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []VSHNMariaDBBackup `json:"items,omitempty"` +} + +// GetGroupVersionResource returns the GroupVersionResource for this resource. +// The resource should be the all lowercase and pluralized kind +func (in *VSHNMariaDBBackup) GetGroupVersionResource() schema.GroupVersionResource { + return schema.GroupVersionResource{ + Group: GroupVersion.Group, + Version: GroupVersion.Version, + Resource: "vshnmariadbbackups", + } +} + +func (in *VSHNMariaDBBackup) GetObjectMeta() *metav1.ObjectMeta { + return &in.ObjectMeta +} + +// IsStorageVersion returns true if the object is also the internal version -- i.e. is the type defined for the API group or an alias to this object. +// If false, the resource is expected to implement MultiVersionObject interface. +func (in *VSHNMariaDBBackup) IsStorageVersion() bool { + return true +} + +func (in *VSHNMariaDBBackup) NamespaceScoped() bool { + return true +} + +func (in *VSHNMariaDBBackup) New() runtime.Object { + return &VSHNMariaDBBackup{} +} + +func (in *VSHNMariaDBBackup) NewList() runtime.Object { + return &VSHNMariaDBBackupList{} +} + +func (in *VSHNMariaDBBackupList) GetListMeta() *metav1.ListMeta { + return &in.ListMeta +} diff --git a/apis/appcat/v1/vshn_postgres_backup_types.go b/apis/appcat/v1/vshn_postgres_backup_types.go index 153c005..f709f62 100644 --- a/apis/appcat/v1/vshn_postgres_backup_types.go +++ b/apis/appcat/v1/vshn_postgres_backup_types.go @@ -23,6 +23,8 @@ var ( Timing = "timing" // End holds field path name end End = "end" + // Start holds field path name start + Start = "start" ) // VSHNPostgreSQLName represents the name of a VSHNPostgreSQL diff --git a/apis/appcat/v1/zz_generated.deepcopy.go b/apis/appcat/v1/zz_generated.deepcopy.go index 55758a1..cebfde1 100644 --- a/apis/appcat/v1/zz_generated.deepcopy.go +++ b/apis/appcat/v1/zz_generated.deepcopy.go @@ -134,6 +134,80 @@ func (in *SGBackupInfo) DeepCopy() *SGBackupInfo { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VSHNMariaDBBackup) DeepCopyInto(out *VSHNMariaDBBackup) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSHNMariaDBBackup. +func (in *VSHNMariaDBBackup) DeepCopy() *VSHNMariaDBBackup { + if in == nil { + return nil + } + out := new(VSHNMariaDBBackup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VSHNMariaDBBackup) 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 *VSHNMariaDBBackupList) DeepCopyInto(out *VSHNMariaDBBackupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VSHNMariaDBBackup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSHNMariaDBBackupList. +func (in *VSHNMariaDBBackupList) DeepCopy() *VSHNMariaDBBackupList { + if in == nil { + return nil + } + out := new(VSHNMariaDBBackupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VSHNMariaDBBackupList) 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 *VSHNMariaDBBackupStatus) DeepCopyInto(out *VSHNMariaDBBackupStatus) { + *out = *in + in.Date.DeepCopyInto(&out.Date) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSHNMariaDBBackupStatus. +func (in *VSHNMariaDBBackupStatus) DeepCopy() *VSHNMariaDBBackupStatus { + if in == nil { + return nil + } + out := new(VSHNMariaDBBackupStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VSHNPlan) DeepCopyInto(out *VSHNPlan) { *out = *in diff --git a/apis/vshn/v1/dbaas_vshn_mariadb.go b/apis/vshn/v1/dbaas_vshn_mariadb.go new file mode 100644 index 0000000..4f4110b --- /dev/null +++ b/apis/vshn/v1/dbaas_vshn_mariadb.go @@ -0,0 +1,130 @@ +package v1 + +import ( + v1 "github.com/vshn/appcat-apiserver/apis/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Workaround to make nested defaulting work. +// kubebuilder is unable to set a {} default +//go:generate yq -i e ../../generated/vshn.appcat.vshn.io_vshnmariadbs.yaml --expression "with(.spec.versions[]; .schema.openAPIV3Schema.properties.spec.properties.parameters.default={})" +//go:generate yq -i e ../../generated/vshn.appcat.vshn.io_vshnmariadbs.yaml --expression "with(.spec.versions[]; .schema.openAPIV3Schema.properties.spec.properties.parameters.properties.size.default={})" +//go:generate yq -i e ../../generated/vshn.appcat.vshn.io_vshnmariadbs.yaml --expression "with(.spec.versions[]; .schema.openAPIV3Schema.properties.spec.properties.parameters.properties.service.default={})" +//go:generate yq -i e ../../generated/vshn.appcat.vshn.io_vshnmariadbs.yaml --expression "with(.spec.versions[]; .schema.openAPIV3Schema.properties.spec.properties.parameters.properties.tls.default={})" +//go:generate yq -i e ../../generated/vshn.appcat.vshn.io_vshnmariadbs.yaml --expression "with(.spec.versions[]; .schema.openAPIV3Schema.properties.spec.properties.parameters.properties.backup.default={})" + +// +kubebuilder:object:root=true + +// VSHNMariaDB is the API for creating MariaDB clusters. +type VSHNMariaDB struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the desired state of a VSHNMariaDB. + Spec VSHNMariaDBSpec `json:"spec"` + + // Status reflects the observed state of a VSHNMariaDB. + Status VSHNMariaDBStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:generate=true +// +kubebuilder:object:root=true +type VSHNMariaDBList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []VSHNMariaDB `json:"items,omitempty"` +} + +// VSHNMariaDBSpec defines the desired state of a VSHNMariaDB. +type VSHNMariaDBSpec struct { + // Parameters are the configurable fields of a VSHNMariaDB. + Parameters VSHNMariaDBParameters `json:"parameters,omitempty"` + + // WriteConnectionSecretToRef references a secret to which the connection details will be written. + WriteConnectionSecretToRef v1.LocalObjectReference `json:"writeConnectionSecretToRef,omitempty"` +} + +// VSHNMariaDBParameters are the configurable fields of a VSHNMariaDB. +type VSHNMariaDBParameters struct { + // Service contains MariaDB DBaaS specific properties + Service VSHNMariaDBServiceSpec `json:"service,omitempty"` + + // Size contains settings to control the sizing of a service. + Size VSHNMariaDBSizeSpec `json:"size,omitempty"` + + // Scheduling contains settings to control the scheduling of an instance. + Scheduling VSHNDBaaSSchedulingSpec `json:"scheduling,omitempty"` + + // TLS contains settings to control tls traffic of a service. + TLS VSHNMariaDBTLSSpec `json:"tls,omitempty"` + + // Backup contains settings to control how the instance should get backed up. + Backup K8upBackupSpec `json:"backup,omitempty"` + + // Restore contains settings to control the restore of an instance. + Restore K8upRestoreSpec `json:"restore,omitempty"` + + // Maintenance contains settings to control the maintenance of an instance. + Maintenance VSHNDBaaSMaintenanceScheduleSpec `json:"maintenance,omitempty"` +} + +// VSHNMariaDBServiceSpec contains MariaDB DBaaS specific properties +type VSHNMariaDBServiceSpec struct { + // +kubebuilder:validation:Enum="6.2";"7.0" + // +kubebuilder:default="7.0" + + // Version contains supported version of MariaDB. + // Multiple versions are supported. The latest version "7.0" is the default version. + Version string `json:"version,omitempty"` + + // MariaDBSettings contains additional MariaDB settings. + MariaDBSettings string `json:"MariaDBSettings,omitempty"` +} + +// VSHNMariaDBSizeSpec contains settings to control the sizing of a service. +type VSHNMariaDBSizeSpec struct { + + // CPURequests defines the requests amount of Kubernetes CPUs for an instance. + CPURequests string `json:"cpuRequests,omitempty"` + + // CPULimits defines the limits amount of Kubernetes CPUs for an instance. + CPULimits string `json:"cpuLimits,omitempty"` + + // MemoryRequests defines the requests amount of memory in units of bytes for an instance. + MemoryRequests string `json:"memoryRequests,omitempty"` + + // MemoryLimits defines the limits amount of memory in units of bytes for an instance. + MemoryLimits string `json:"memoryLimits,omitempty"` + + // Disk defines the amount of disk space for an instance. + Disk string `json:"disk,omitempty"` + + // Plan is the name of the resource plan that defines the compute resources. + Plan string `json:"plan,omitempty"` +} + +// VSHNMariaDBTLSSpec contains settings to control tls traffic of a service. +type VSHNMariaDBTLSSpec struct { + // +kubebuilder:default=true + + // TLSEnabled enables TLS traffic for the service + TLSEnabled bool `json:"enabled,omitempty"` + + // +kubebuilder:default=true + // TLSAuthClients enables client authentication requirement + TLSAuthClients bool `json:"authClients,omitempty"` +} + +// VSHNMariaDBStatus reflects the observed state of a VSHNMariaDB. +type VSHNMariaDBStatus struct { + // MariaDBConditions contains the status conditions of the backing object. + NamespaceConditions []v1.Condition `json:"namespaceConditions,omitempty"` + SelfSignedIssuerConditions []v1.Condition `json:"selfSignedIssuerConditions,omitempty"` + LocalCAConditions []v1.Condition `json:"localCAConditions,omitempty"` + CaCertificateConditions []v1.Condition `json:"caCertificateConditions,omitempty"` + ServerCertificateConditions []v1.Condition `json:"serverCertificateConditions,omitempty"` + ClientCertificateConditions []v1.Condition `json:"clientCertificateConditions,omitempty"` + // InstanceNamespace contains the name of the namespace where the instance resides + InstanceNamespace string `json:"instanceNamespace,omitempty"` +} diff --git a/apis/vshn/v1/groupversion_info.go b/apis/vshn/v1/groupversion_info.go index a574426..76af95a 100644 --- a/apis/vshn/v1/groupversion_info.go +++ b/apis/vshn/v1/groupversion_info.go @@ -28,5 +28,7 @@ func init() { &XVSHNPostgreSQLList{}, &VSHNRedis{}, &VSHNRedisList{}, + &VSHNMariaDB{}, + &VSHNMariaDBList{}, ) } diff --git a/apis/vshn/v1/zz_generated.deepcopy.go b/apis/vshn/v1/zz_generated.deepcopy.go index 28e782a..0b0dd59 100644 --- a/apis/vshn/v1/zz_generated.deepcopy.go +++ b/apis/vshn/v1/zz_generated.deepcopy.go @@ -159,6 +159,206 @@ func (in *VSHNDBaaSSizeSpec) DeepCopy() *VSHNDBaaSSizeSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VSHNMariaDB) DeepCopyInto(out *VSHNMariaDB) { + *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 VSHNMariaDB. +func (in *VSHNMariaDB) DeepCopy() *VSHNMariaDB { + if in == nil { + return nil + } + out := new(VSHNMariaDB) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VSHNMariaDB) 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 *VSHNMariaDBList) DeepCopyInto(out *VSHNMariaDBList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VSHNMariaDB, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSHNMariaDBList. +func (in *VSHNMariaDBList) DeepCopy() *VSHNMariaDBList { + if in == nil { + return nil + } + out := new(VSHNMariaDBList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VSHNMariaDBList) 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 *VSHNMariaDBParameters) DeepCopyInto(out *VSHNMariaDBParameters) { + *out = *in + out.Service = in.Service + out.Size = in.Size + in.Scheduling.DeepCopyInto(&out.Scheduling) + out.TLS = in.TLS + out.Backup = in.Backup + out.Restore = in.Restore + out.Maintenance = in.Maintenance +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSHNMariaDBParameters. +func (in *VSHNMariaDBParameters) DeepCopy() *VSHNMariaDBParameters { + if in == nil { + return nil + } + out := new(VSHNMariaDBParameters) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VSHNMariaDBServiceSpec) DeepCopyInto(out *VSHNMariaDBServiceSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSHNMariaDBServiceSpec. +func (in *VSHNMariaDBServiceSpec) DeepCopy() *VSHNMariaDBServiceSpec { + if in == nil { + return nil + } + out := new(VSHNMariaDBServiceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VSHNMariaDBSizeSpec) DeepCopyInto(out *VSHNMariaDBSizeSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSHNMariaDBSizeSpec. +func (in *VSHNMariaDBSizeSpec) DeepCopy() *VSHNMariaDBSizeSpec { + if in == nil { + return nil + } + out := new(VSHNMariaDBSizeSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VSHNMariaDBSpec) DeepCopyInto(out *VSHNMariaDBSpec) { + *out = *in + in.Parameters.DeepCopyInto(&out.Parameters) + out.WriteConnectionSecretToRef = in.WriteConnectionSecretToRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSHNMariaDBSpec. +func (in *VSHNMariaDBSpec) DeepCopy() *VSHNMariaDBSpec { + if in == nil { + return nil + } + out := new(VSHNMariaDBSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VSHNMariaDBStatus) DeepCopyInto(out *VSHNMariaDBStatus) { + *out = *in + if in.NamespaceConditions != nil { + in, out := &in.NamespaceConditions, &out.NamespaceConditions + *out = make([]apisv1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.SelfSignedIssuerConditions != nil { + in, out := &in.SelfSignedIssuerConditions, &out.SelfSignedIssuerConditions + *out = make([]apisv1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.LocalCAConditions != nil { + in, out := &in.LocalCAConditions, &out.LocalCAConditions + *out = make([]apisv1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.CaCertificateConditions != nil { + in, out := &in.CaCertificateConditions, &out.CaCertificateConditions + *out = make([]apisv1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ServerCertificateConditions != nil { + in, out := &in.ServerCertificateConditions, &out.ServerCertificateConditions + *out = make([]apisv1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ClientCertificateConditions != nil { + in, out := &in.ClientCertificateConditions, &out.ClientCertificateConditions + *out = make([]apisv1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSHNMariaDBStatus. +func (in *VSHNMariaDBStatus) DeepCopy() *VSHNMariaDBStatus { + if in == nil { + return nil + } + out := new(VSHNMariaDBStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VSHNMariaDBTLSSpec) DeepCopyInto(out *VSHNMariaDBTLSSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSHNMariaDBTLSSpec. +func (in *VSHNMariaDBTLSSpec) DeepCopy() *VSHNMariaDBTLSSpec { + if in == nil { + return nil + } + out := new(VSHNMariaDBTLSSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VSHNPostgreSQL) DeepCopyInto(out *VSHNPostgreSQL) { *out = *in diff --git a/cmd/apiserver.go b/cmd/apiserver.go index c93aabd..8c9af79 100644 --- a/cmd/apiserver.go +++ b/cmd/apiserver.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/viper" appcatv1 "github.com/vshn/appcat-apiserver/apis/appcat/v1" "github.com/vshn/appcat-apiserver/pkg/apiserver/appcat" + vshnmariadb "github.com/vshn/appcat-apiserver/pkg/apiserver/vshn/mariadb" vshnpostgres "github.com/vshn/appcat-apiserver/pkg/apiserver/vshn/postgres" vshnredis "github.com/vshn/appcat-apiserver/pkg/apiserver/vshn/redis" "sigs.k8s.io/apiserver-runtime/pkg/builder" @@ -21,7 +22,7 @@ func newAPIServerCMD() *cobra.Command { viper.AutomaticEnv() - var appcatEnabled, vshnPGBackupsEnabled, vshnRedisBackupsEnabled bool + var appcatEnabled, vshnPGBackupsEnabled, vshnRedisBackupsEnabled, vshnMariaDBBackupEnabled bool if len(os.Args) < 2 { return &cobra.Command{} @@ -31,7 +32,8 @@ func newAPIServerCMD() *cobra.Command { appcatEnabled = viper.GetBool("APPCAT_HANDLER_ENABLED") vshnPGBackupsEnabled = viper.GetBool("VSHN_POSTGRES_BACKUP_HANDLER_ENABLED") vshnRedisBackupsEnabled = viper.GetBool("VSHN_REDIS_BACKUP_HANDLER_ENABLED") - if !appcatEnabled && !vshnPGBackupsEnabled && !vshnRedisBackupsEnabled { + vshnMariaDBBackupEnabled = viper.GetBool("VSHN_MARIADB_BACKUP_HANDLER_ENABLED") + if !appcatEnabled && !vshnPGBackupsEnabled && !vshnRedisBackupsEnabled && !vshnMariaDBBackupEnabled { log.Fatal("Handlers are not enabled, please set at least one of APPCAT_HANDLER_ENABLED | VSHN_POSTGRES_BACKUP_HANDLER_ENABLED | VSHN_REDIS_BACKUP_HANDLER_ENABLED env variables to True") } } @@ -50,6 +52,10 @@ func newAPIServerCMD() *cobra.Command { b.WithResourceAndHandler(&appcatv1.VSHNRedisBackup{}, vshnredis.New()) } + if vshnMariaDBBackupEnabled { + b.WithResourceAndHandler(&appcatv1.VSHNMariaDBBackup{}, vshnmariadb.New()) + } + b.WithoutEtcd(). ExposeLoopbackAuthorizer(). ExposeLoopbackMasterClientConfig() diff --git a/config/apiserver/role.yaml b/config/apiserver/role.yaml index 1f9a9bb..e1cd984 100644 --- a/config/apiserver/role.yaml +++ b/config/apiserver/role.yaml @@ -72,6 +72,7 @@ rules: - apiGroups: - vshn.appcat.vshn.io resources: + - vshnmariadbs - vshnredis - xvshnpostgresqls verbs: diff --git a/pkg/apiserver/common.go b/pkg/apiserver/common.go index c258d85..708bcfc 100644 --- a/pkg/apiserver/common.go +++ b/pkg/apiserver/common.go @@ -2,6 +2,8 @@ package apiserver import ( "errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "sync" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -67,3 +69,22 @@ func (m *MultiWatcher) ResultChan() <-chan watch.Event { } return m.eventChan } + +func GetBackupTable(id, instance, status, age, started, finished string, backup runtime.Object) metav1.TableRow { + return metav1.TableRow{ + Cells: []interface{}{id, instance, started, finished, status, age}, // Snapshots are created only when the backup successfully finished + Object: runtime.RawExtension{Object: backup}, + } +} + +func GetBackupColumnDefinition() []metav1.TableColumnDefinition { + desc := metav1.ObjectMeta{}.SwaggerDoc() + return []metav1.TableColumnDefinition{ + {Name: "Backup ID", Type: "string", Format: "name", Description: desc["name"]}, + {Name: "Database Instance", Type: "string", Description: "The database instance"}, + {Name: "Started", Type: "string", Description: "The backup start time"}, + {Name: "Finished", Type: "string", Description: "The data is available up to this time"}, + {Name: "Status", Type: "string", Description: "The state of this backup"}, + {Name: "Age", Type: "date", Description: desc["creationTimestamp"]}, + } +} diff --git a/pkg/apiserver/vshn/mariadb/backup.go b/pkg/apiserver/vshn/mariadb/backup.go new file mode 100644 index 0000000..06c8c0b --- /dev/null +++ b/pkg/apiserver/vshn/mariadb/backup.go @@ -0,0 +1,73 @@ +package mariadb + +import ( + k8upv1 "github.com/k8up-io/k8up/v2/api/v1" + appcatv1 "github.com/vshn/appcat-apiserver/apis/appcat/v1" + "github.com/vshn/appcat-apiserver/pkg/apiserver/vshn/k8up" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + genericregistry "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/registry/rest" + restbuilder "sigs.k8s.io/apiserver-runtime/pkg/builder/rest" + "sigs.k8s.io/apiserver-runtime/pkg/util/loopback" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ rest.Scoper = &vshnMariaDBBackupStorage{} +var _ rest.Storage = &vshnMariaDBBackupStorage{} + +type vshnMariaDBBackupStorage struct { + snapshothandler k8up.Snapshothandler + vshnMariaDB vshnMariaDBProvider +} + +// New returns a new resthandler for MariaDB backups. +func New() restbuilder.ResourceHandlerProvider { + return func(s *runtime.Scheme, gasdf genericregistry.RESTOptionsGetter) (rest.Storage, error) { + c, err := client.NewWithWatch(loopback.GetLoopbackMasterClientConfig(), client.Options{}) + if err != nil { + return nil, err + } + + _ = k8upv1.AddToScheme(c.Scheme()) + + return &vshnMariaDBBackupStorage{ + snapshothandler: k8up.New(c), + vshnMariaDB: &concreteMariaDBProvider{ + client: c, + }, + }, nil + } +} + +func (v vshnMariaDBBackupStorage) New() runtime.Object { + return &appcatv1.VSHNMariaDBBackup{} +} + +func (v vshnMariaDBBackupStorage) Destroy() {} + +func (v *vshnMariaDBBackupStorage) NamespaceScoped() bool { + return true +} + +func trimStringLength(in string) string { + length := len(in) + if length > 8 { + length = 8 + } + return in[:length] +} + +func deRefString(in *string) string { + if in == nil { + return "" + } + return *in +} + +func deRefMetaTime(in *metav1.Time) metav1.Time { + if in == nil { + return metav1.Now() + } + return *in +} diff --git a/pkg/apiserver/vshn/mariadb/common_test.go b/pkg/apiserver/vshn/mariadb/common_test.go new file mode 100644 index 0000000..30c9635 --- /dev/null +++ b/pkg/apiserver/vshn/mariadb/common_test.go @@ -0,0 +1,64 @@ +package mariadb + +import ( + "context" + + k8upv1 "github.com/k8up-io/k8up/v2/api/v1" + vshnv1 "github.com/vshn/appcat-apiserver/apis/vshn/v1" + "github.com/vshn/appcat-apiserver/pkg/apiserver/vshn/k8up" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + "k8s.io/apimachinery/pkg/watch" +) + +var _ vshnMariaDBProvider = &mockprovider{} +var _ k8up.Snapshothandler = &mockhandler{} + +type mockprovider struct { + err error + instances *vshnv1.VSHNMariaDBList +} + +func (m *mockprovider) ListVSHNMariaDB(ctx context.Context, namespace string) (*vshnv1.VSHNMariaDBList, error) { + + instances := &vshnv1.VSHNMariaDBList{ + Items: []vshnv1.VSHNMariaDB{}, + } + + for _, instance := range m.instances.Items { + if instance.GetNamespace() == namespace { + instances.Items = append(instances.Items, instance) + } + } + + return instances, m.err +} + +type mockhandler struct { + snapshot *k8upv1.Snapshot + snapshots *k8upv1.SnapshotList +} + +func (m *mockhandler) Get(ctx context.Context, id, instanceNamespace string) (*k8upv1.Snapshot, error) { + return m.snapshot, nil +} + +func (m *mockhandler) List(ctx context.Context, instanceNamespace string) (*k8upv1.SnapshotList, error) { + + snapshots := &k8upv1.SnapshotList{ + Items: []k8upv1.Snapshot{}, + } + + for _, snap := range m.snapshots.Items { + if snap.GetNamespace() == instanceNamespace { + snapshots.Items = append(snapshots.Items, snap) + } + } + + return snapshots, nil +} +func (m *mockhandler) Watch(ctx context.Context, namespace string, options *metainternalversion.ListOptions) (watch.Interface, error) { + return nil, nil +} +func (m *mockhandler) GetFromEvent(in watch.Event) (*k8upv1.Snapshot, error) { + return nil, nil +} diff --git a/pkg/apiserver/vshn/mariadb/create.go b/pkg/apiserver/vshn/mariadb/create.go new file mode 100644 index 0000000..7e7344f --- /dev/null +++ b/pkg/apiserver/vshn/mariadb/create.go @@ -0,0 +1,16 @@ +package mariadb + +import ( + "context" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" +) + +var _ rest.Creater = &vshnMariaDBBackupStorage{} + +func (v vshnMariaDBBackupStorage) Create(_ context.Context, _ runtime.Object, _ rest.ValidateObjectFunc, _ *metav1.CreateOptions) (runtime.Object, error) { + return nil, fmt.Errorf("method not implemented") +} diff --git a/pkg/apiserver/vshn/mariadb/delete.go b/pkg/apiserver/vshn/mariadb/delete.go new file mode 100644 index 0000000..fe237c8 --- /dev/null +++ b/pkg/apiserver/vshn/mariadb/delete.go @@ -0,0 +1,29 @@ +package mariadb + +import ( + "context" + + v1 "github.com/vshn/appcat-apiserver/apis/appcat/v1" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" +) + +var _ rest.GracefulDeleter = &vshnMariaDBBackupStorage{} +var _ rest.CollectionDeleter = &vshnMariaDBBackupStorage{} + +func (v vshnMariaDBBackupStorage) Delete(_ context.Context, name string, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions) (runtime.Object, bool, error) { + return &v1.VSHNMariaDBBackup{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }, false, nil +} + +func (v *vshnMariaDBBackupStorage) DeleteCollection(ctx context.Context, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions, _ *metainternalversion.ListOptions) (runtime.Object, error) { + return &v1.VSHNMariaDBBackupList{ + Items: []v1.VSHNMariaDBBackup{}, + }, nil +} diff --git a/pkg/apiserver/vshn/mariadb/get.go b/pkg/apiserver/vshn/mariadb/get.go new file mode 100644 index 0000000..8269ea3 --- /dev/null +++ b/pkg/apiserver/vshn/mariadb/get.go @@ -0,0 +1,55 @@ +package mariadb + +import ( + "context" + "fmt" + + appcatv1 "github.com/vshn/appcat-apiserver/apis/appcat/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" +) + +var _ rest.Getter = &vshnMariaDBBackupStorage{} + +func (v *vshnMariaDBBackupStorage) Get(ctx context.Context, name string, _ *metav1.GetOptions) (runtime.Object, error) { + + namespace, ok := request.NamespaceFrom(ctx) + if !ok { + return nil, fmt.Errorf("cannot get namespace from context") + } + + instances, err := v.vshnMariaDB.ListVSHNMariaDB(ctx, namespace) + if err != nil { + return nil, err + } + + mariadbSnap := &appcatv1.VSHNMariaDBBackup{} + + for _, instance := range instances.Items { + ins := instance.Status.InstanceNamespace + snap, err := v.snapshothandler.Get(ctx, name, ins) + if err != nil { + if apierrors.IsNotFound(err) { + continue + } + return nil, err + } + + backupMeta := snap.ObjectMeta + backupMeta.Namespace = instance.GetNamespace() + + mariadbSnap = &appcatv1.VSHNMariaDBBackup{ + ObjectMeta: backupMeta, + Status: appcatv1.VSHNMariaDBBackupStatus{ + ID: deRefString(snap.Spec.ID), + Date: deRefMetaTime(snap.Spec.Date), + Instance: instance.GetName(), + }, + } + } + + return mariadbSnap, nil +} diff --git a/pkg/apiserver/vshn/mariadb/get_test.go b/pkg/apiserver/vshn/mariadb/get_test.go new file mode 100644 index 0000000..e3345fb --- /dev/null +++ b/pkg/apiserver/vshn/mariadb/get_test.go @@ -0,0 +1,110 @@ +package mariadb + +import ( + "context" + "testing" + "time" + + k8upv1 "github.com/k8up-io/k8up/v2/api/v1" + "github.com/stretchr/testify/assert" + appcatv1 "github.com/vshn/appcat-apiserver/apis/appcat/v1" + vshnv1 "github.com/vshn/appcat-apiserver/apis/vshn/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/utils/pointer" +) + +func Test_vshnMariaDBBackupStorage_Get(t *testing.T) { + tests := []struct { + name string + instanceName string + instances *vshnv1.VSHNMariaDBList + snapshot *k8upv1.Snapshot + wantDate metav1.Time + wantNs string + want runtime.Object + wantErr bool + }{ + { + name: "GivenExistingBackup_ThenExpectBackup", + instances: &vshnv1.VSHNMariaDBList{ + Items: []vshnv1.VSHNMariaDB{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "myinstance", + Namespace: "ns1", + }, + Status: vshnv1.VSHNMariaDBStatus{ + InstanceNamespace: "ins1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "myinstance", + Namespace: "ns2", + }, + Status: vshnv1.VSHNMariaDBStatus{ + InstanceNamespace: "ins2", + }, + }, + }, + }, + snapshot: &k8upv1.Snapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myid", + Namespace: "ns1", + }, + Spec: k8upv1.SnapshotSpec{ + ID: pointer.String("myid"), + }, + }, + wantDate: metav1.Date(2023, time.April, 3, 13, 37, 0, 0, time.UTC), + wantNs: "ns1", + want: &appcatv1.VSHNMariaDBBackup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myid", + Namespace: "ns1", + }, + Status: appcatv1.VSHNMariaDBBackupStatus{ + ID: "myid", + Instance: "myinstance", + Date: metav1.Date(2023, time.April, 3, 13, 37, 0, 0, time.UTC), + }, + }, + }, + { + name: "GivenNoBackup_ThenExpectEmptyObjects", + instances: &vshnv1.VSHNMariaDBList{}, + snapshot: nil, + want: &appcatv1.VSHNMariaDBBackup{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + ctx := request.WithNamespace(context.TODO(), tt.wantNs) + + if tt.snapshot != nil { + tt.snapshot.Spec.Date = &tt.wantDate + } + + v := &vshnMariaDBBackupStorage{ + vshnMariaDB: &mockprovider{ + instances: tt.instances, + }, + snapshothandler: &mockhandler{ + snapshot: tt.snapshot, + }, + } + got, err := v.Get(ctx, tt.instanceName, &metav1.GetOptions{}) + if (err != nil) != tt.wantErr { + t.Errorf("vshnMariaDBBackupStorage.Get() error = %v, wantErr %v", err, tt.wantErr) + return + } + + assert.Equal(t, tt.want, got) + + }) + } +} diff --git a/pkg/apiserver/vshn/mariadb/list.go b/pkg/apiserver/vshn/mariadb/list.go new file mode 100644 index 0000000..e4ad92f --- /dev/null +++ b/pkg/apiserver/vshn/mariadb/list.go @@ -0,0 +1,61 @@ +package mariadb + +import ( + "context" + "fmt" + + appcatv1 "github.com/vshn/appcat-apiserver/apis/appcat/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" +) + +var _ rest.Lister = &vshnMariaDBBackupStorage{} + +func (v *vshnMariaDBBackupStorage) List(ctx context.Context, _ *metainternalversion.ListOptions) (runtime.Object, error) { + + namespace, ok := request.NamespaceFrom(ctx) + if !ok { + return nil, fmt.Errorf("cannot get namespace from resource") + } + + instances, err := v.vshnMariaDB.ListVSHNMariaDB(ctx, namespace) + if err != nil { + return nil, err + } + + mariadbSnapshots := &appcatv1.VSHNMariaDBBackupList{ + Items: []appcatv1.VSHNMariaDBBackup{}, + } + + for _, instance := range instances.Items { + snapshots, err := v.snapshothandler.List(ctx, instance.Status.InstanceNamespace) + if err != nil && !apierrors.IsNotFound(err) { + return nil, err + } + + for _, snap := range snapshots.Items { + + backupMeta := snap.ObjectMeta + backupMeta.Namespace = instance.GetNamespace() + + mariadbSnapshots.Items = append(mariadbSnapshots.Items, appcatv1.VSHNMariaDBBackup{ + ObjectMeta: backupMeta, + Status: appcatv1.VSHNMariaDBBackupStatus{ + ID: deRefString(snap.Spec.ID), + Date: deRefMetaTime(snap.Spec.Date), + Instance: instance.GetName(), + }, + }) + } + + } + + return mariadbSnapshots, nil +} + +func (v *vshnMariaDBBackupStorage) NewList() runtime.Object { + return &appcatv1.VSHNMariaDBBackupList{} +} diff --git a/pkg/apiserver/vshn/mariadb/list_test.go b/pkg/apiserver/vshn/mariadb/list_test.go new file mode 100644 index 0000000..d715c4e --- /dev/null +++ b/pkg/apiserver/vshn/mariadb/list_test.go @@ -0,0 +1,154 @@ +package mariadb + +import ( + "context" + "testing" + "time" + + k8upv1 "github.com/k8up-io/k8up/v2/api/v1" + "github.com/stretchr/testify/assert" + appcatv1 "github.com/vshn/appcat-apiserver/apis/appcat/v1" + vshnv1 "github.com/vshn/appcat-apiserver/apis/vshn/v1" + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/utils/pointer" +) + +func Test_vshnMariaDBBackupStorage_List(t *testing.T) { + tests := []struct { + name string + instances *vshnv1.VSHNMariaDBList + snapshots *k8upv1.SnapshotList + wantNs string + want runtime.Object + wantDate metav1.Time + wantErr bool + }{ + { + name: "GivenAvailableBackups_ThenExpectBackupList", + instances: &vshnv1.VSHNMariaDBList{ + Items: []vshnv1.VSHNMariaDB{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "myinstance", + Namespace: "ns1", + }, + Status: vshnv1.VSHNMariaDBStatus{ + InstanceNamespace: "ins1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "mysecondinstance", + Namespace: "ns1", + }, + Status: vshnv1.VSHNMariaDBStatus{ + InstanceNamespace: "ins2", + }, + }, + }, + }, + snapshots: &k8upv1.SnapshotList{ + Items: []k8upv1.Snapshot{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "snap1", + Namespace: "ins1", + }, + Spec: k8upv1.SnapshotSpec{ + ID: pointer.String("snap1"), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "snap2", + Namespace: "ins2", + }, + Spec: k8upv1.SnapshotSpec{ + ID: pointer.String("snap2"), + }, + }, + }, + }, + wantNs: "ns1", + wantDate: metav1.Date(2023, time.April, 3, 13, 37, 0, 0, time.UTC), + want: &appcatv1.VSHNMariaDBBackupList{ + Items: []appcatv1.VSHNMariaDBBackup{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "snap1", + Namespace: "ns1", + }, + Status: appcatv1.VSHNMariaDBBackupStatus{ + ID: "snap1", + Instance: "myinstance", + Date: metav1.Date(2023, time.April, 3, 13, 37, 0, 0, time.UTC), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "snap2", + Namespace: "ns1", + }, + Status: appcatv1.VSHNMariaDBBackupStatus{ + ID: "snap2", + Instance: "mysecondinstance", + Date: metav1.Date(2023, time.April, 3, 13, 37, 0, 0, time.UTC), + }, + }, + }, + }, + }, + { + name: "GivenNobackups_ThenExpectEmptyList", + instances: &vshnv1.VSHNMariaDBList{ + Items: []vshnv1.VSHNMariaDB{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "myinstance", + Namespace: "ns1", + }, + Status: vshnv1.VSHNMariaDBStatus{ + InstanceNamespace: "ins1", + }, + }, + }, + }, + snapshots: &k8upv1.SnapshotList{ + Items: []k8upv1.Snapshot{}, + }, + wantNs: "ns1", + want: &appcatv1.VSHNMariaDBBackupList{ + Items: []appcatv1.VSHNMariaDBBackup{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + ctx := request.WithNamespace(context.TODO(), tt.wantNs) + + for i := range tt.snapshots.Items { + tt.snapshots.Items[i].Spec.Date = &tt.wantDate + } + + v := &vshnMariaDBBackupStorage{ + snapshothandler: &mockhandler{ + snapshots: tt.snapshots, + }, + vshnMariaDB: &mockprovider{ + instances: tt.instances, + }, + } + got, err := v.List(ctx, &internalversion.ListOptions{}) + if (err != nil) != tt.wantErr { + t.Errorf("vshnMariaDBBackupStorage.List() error = %v, wantErr %v", err, tt.wantErr) + return + } + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/apiserver/vshn/mariadb/table.go b/pkg/apiserver/vshn/mariadb/table.go new file mode 100644 index 0000000..b6c642d --- /dev/null +++ b/pkg/apiserver/vshn/mariadb/table.go @@ -0,0 +1,60 @@ +package mariadb + +import ( + "context" + "fmt" + "github.com/vshn/appcat-apiserver/pkg/apiserver" + "k8s.io/apimachinery/pkg/util/duration" + "time" + + appcatv1 "github.com/vshn/appcat-apiserver/apis/appcat/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" +) + +var _ rest.TableConvertor = &vshnMariaDBBackupStorage{} + +func (v *vshnMariaDBBackupStorage) ConvertToTable(_ context.Context, obj runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + + table := &metav1.Table{} + + backups := []appcatv1.VSHNMariaDBBackup{} + if meta.IsListType(obj) { + backupList, ok := obj.(*appcatv1.VSHNMariaDBBackupList) + if !ok { + return nil, fmt.Errorf("not a vshn mariadb backup: %#v", obj) + } + backups = backupList.Items + } else { + backup, ok := obj.(*appcatv1.VSHNMariaDBBackup) + if !ok { + return nil, fmt.Errorf("not a vshn mariadb backup: %#v", obj) + } + backups = append(backups, *backup) + } + + for i := range backups { + table.Rows = append(table.Rows, backupToTableRow(&backups[i])) + } + + if opt, ok := tableOptions.(*metav1.TableOptions); !ok || !opt.NoHeaders { + table.ColumnDefinitions = apiserver.GetBackupColumnDefinition() + } + + return table, nil +} + +// ToDo Once k8up exposes start time, update the code here +func backupToTableRow(backup *appcatv1.VSHNMariaDBBackup) metav1.TableRow { + return apiserver.GetBackupTable( + trimStringLength(backup.Status.ID), + backup.Status.Instance, + "Completed", + duration.HumanDuration(time.Since(backup.GetCreationTimestamp().Time)), + backup.Status.Date.Format(time.RFC3339), + backup.Status.Date.Format(time.RFC3339), + backup, + ) +} diff --git a/pkg/apiserver/vshn/mariadb/table_test.go b/pkg/apiserver/vshn/mariadb/table_test.go new file mode 100644 index 0000000..0775bb6 --- /dev/null +++ b/pkg/apiserver/vshn/mariadb/table_test.go @@ -0,0 +1,109 @@ +package mariadb + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + vshnv1 "github.com/vshn/appcat-apiserver/apis/appcat/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func Test_vshnMariaDBBackupStorage_ConvertToTable(t *testing.T) { + tests := []struct { + name string + obj runtime.Object + tableOptions runtime.Object + wantRows int + wantErr bool + }{ + { + name: "GivenOneBackup_ThenExpectOneRow", + obj: &vshnv1.VSHNMariaDBBackup{}, + wantRows: 1, + }, + { + name: "GivenMiltipleBackups_ThenExpectMultipleRows", + obj: &vshnv1.VSHNMariaDBBackupList{ + Items: []vshnv1.VSHNMariaDBBackup{ + {}, + {}, + {}, + }, + }, + wantRows: 3, + }, + { + name: "GivenNoBackupObject_ThenExpectError", + obj: &vshnv1.AppCat{}, + wantErr: true, + }, + { + name: "GivenNil_ThenExpectError", + obj: nil, + wantErr: true, + }, + { + name: "GivenNonBackupList_THenExpectError", + obj: &vshnv1.VSHNPostgresBackupList{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &vshnMariaDBBackupStorage{} + got, err := v.ConvertToTable(context.TODO(), tt.obj, tt.tableOptions) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + assert.Len(t, got.Rows, tt.wantRows) + }) + } +} + +func Test_vshnMariaDBBackupStorage_ConvertToTable_noduplicate(t *testing.T) { + v := &vshnMariaDBBackupStorage{} + obj := vshnv1.VSHNMariaDBBackupList{ + Items: []vshnv1.VSHNMariaDBBackup{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "foo1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "foo2", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "foo3", + }, + }, + }, + } + got, err := v.ConvertToTable(context.TODO(), &obj, nil) + assert.NoError(t, err) + assert.Len(t, got.Rows, 3) + + foo1, ok := got.Rows[0].Object.Object.(*vshnv1.VSHNMariaDBBackup) + if assert.True(t, ok, "unexpected type for foo1") { + assert.Equal(t, "foo1", foo1.Namespace) + } + foo2, ok := got.Rows[1].Object.Object.(*vshnv1.VSHNMariaDBBackup) + if assert.True(t, ok, "unexpected type for foo1") { + assert.Equal(t, "foo2", foo2.Namespace) + } + foo3, ok := got.Rows[2].Object.Object.(*vshnv1.VSHNMariaDBBackup) + if assert.True(t, ok, "unexpected type for foo1") { + assert.Equal(t, "foo3", foo3.Namespace) + } + +} diff --git a/pkg/apiserver/vshn/mariadb/update.go b/pkg/apiserver/vshn/mariadb/update.go new file mode 100644 index 0000000..5133b6e --- /dev/null +++ b/pkg/apiserver/vshn/mariadb/update.go @@ -0,0 +1,18 @@ +package mariadb + +import ( + "context" + "fmt" + + v1 "github.com/vshn/appcat-apiserver/apis/appcat/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" +) + +var _ rest.Updater = &vshnMariaDBBackupStorage{} +var _ rest.CreaterUpdater = &vshnMariaDBBackupStorage{} + +func (v vshnMariaDBBackupStorage) Update(_ context.Context, name string, _ rest.UpdatedObjectInfo, _ rest.ValidateObjectFunc, _ rest.ValidateObjectUpdateFunc, _ bool, _ *metav1.UpdateOptions) (runtime.Object, bool, error) { + return &v1.VSHNPostgresBackup{}, false, fmt.Errorf("method not implemented") +} diff --git a/pkg/apiserver/vshn/mariadb/vshnmariadb.go b/pkg/apiserver/vshn/mariadb/vshnmariadb.go new file mode 100644 index 0000000..069df37 --- /dev/null +++ b/pkg/apiserver/vshn/mariadb/vshnmariadb.go @@ -0,0 +1,39 @@ +package mariadb + +import ( + "context" + + vshnv1 "github.com/vshn/appcat-apiserver/apis/vshn/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type vshnMariaDBProvider interface { + ListVSHNMariaDB(ctx context.Context, namespace string) (*vshnv1.VSHNMariaDBList, error) +} + +type concreteMariaDBProvider struct { + client client.Client +} + +func (c *concreteMariaDBProvider) ListVSHNMariaDB(ctx context.Context, namespace string) (*vshnv1.VSHNMariaDBList, error) { + + instances := &vshnv1.VSHNMariaDBList{} + + err := c.client.List(ctx, instances, &client.ListOptions{Namespace: namespace}) + if err != nil { + return nil, err + } + + cleanedList := make([]vshnv1.VSHNMariaDB, 0) + for _, p := range instances.Items { + // + // In some cases instance namespaces is missing and as a consequence all backups from the whole cluster + // are being exposed creating a security issue - check APPCAT-563. + if p.Status.InstanceNamespace != "" { + cleanedList = append(cleanedList, p) + } + } + instances.Items = cleanedList + + return instances, nil +} diff --git a/pkg/apiserver/vshn/mariadb/watch.go b/pkg/apiserver/vshn/mariadb/watch.go new file mode 100644 index 0000000..7784fdb --- /dev/null +++ b/pkg/apiserver/vshn/mariadb/watch.go @@ -0,0 +1,76 @@ +package mariadb + +import ( + "context" + "fmt" + + appcatv1 "github.com/vshn/appcat-apiserver/apis/appcat/v1" + v1 "github.com/vshn/appcat-apiserver/apis/appcat/v1" + "github.com/vshn/appcat-apiserver/pkg/apiserver" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" +) + +var _ rest.Watcher = &vshnMariaDBBackupStorage{} + +func (v *vshnMariaDBBackupStorage) Watch(ctx context.Context, options *metainternalversion.ListOptions) (watch.Interface, error) { + namespace, ok := request.NamespaceFrom(ctx) + if !ok { + return nil, fmt.Errorf("cannot get namespace from resource") + } + + instances, err := v.vshnMariaDB.ListVSHNMariaDB(ctx, namespace) + if err != nil { + return nil, fmt.Errorf("cannot list VSHNPostgreSQL instances") + } + + mw := apiserver.NewEmptyMultiWatch() + for _, value := range instances.Items { + backupWatcher, err := v.snapshothandler.Watch(ctx, value.Status.InstanceNamespace, options) + if err != nil { + return nil, apiserver.ResolveError(v1.GetGroupResource(v1.ResourceBackup), err) + } + mw.AddWatcher(backupWatcher) + } + + return watch.Filter(mw, func(in watch.Event) (out watch.Event, keep bool) { + if in.Object == nil { + // This should never happen, let downstream deal with it + return in, true + } + + backupInfo, err := v.snapshothandler.GetFromEvent(in) + if err != nil { + return in, false + } + + db := "" + namespace := "" + for _, value := range instances.Items { + if value.Status.InstanceNamespace == backupInfo.GetNamespace() { + db = value.GetName() + namespace = value.GetNamespace() + } + } + + if db == "" { + return in, false + } + + backupMeta := backupInfo.ObjectMeta + backupMeta.Namespace = namespace + + in.Object = &appcatv1.VSHNMariaDBBackup{ + ObjectMeta: backupMeta, + Status: v1.VSHNMariaDBBackupStatus{ + ID: deRefString(backupInfo.Spec.ID), + Date: deRefMetaTime(backupInfo.Spec.Date), + Instance: db, + }, + } + + return in, true + }), nil +} diff --git a/pkg/apiserver/vshn/postgres/table.go b/pkg/apiserver/vshn/postgres/table.go index 9d359ab..4bd289f 100644 --- a/pkg/apiserver/vshn/postgres/table.go +++ b/pkg/apiserver/vshn/postgres/table.go @@ -3,6 +3,7 @@ package postgres import ( "context" "fmt" + "github.com/vshn/appcat-apiserver/pkg/apiserver" "time" v1 "github.com/vshn/appcat-apiserver/apis/appcat/v1" @@ -40,28 +41,20 @@ func (v *vshnPostgresBackupStorage) ConvertToTable(_ context.Context, obj runtim } if opt, ok := tableOptions.(*metav1.TableOptions); !ok || !opt.NoHeaders { - desc := metav1.ObjectMeta{}.SwaggerDoc() - table.ColumnDefinitions = []metav1.TableColumnDefinition{ - {Name: "Backup Name", Type: "string", Format: "name", Description: desc["name"]}, - {Name: "Database Instance", Type: "string", Description: "The database instance"}, - {Name: "Finished On", Type: "string", Description: "The data is available up to this time"}, - {Name: "Status", Type: "string", Description: "The state of this backup"}, - {Name: "Age", Type: "date", Description: desc["creationTimestamp"]}, - } + table.ColumnDefinitions = apiserver.GetBackupColumnDefinition() } return &table, nil } func backupToTableRow(backup *v1.VSHNPostgresBackup) metav1.TableRow { - return metav1.TableRow{ - Cells: []interface{}{ - backup.GetName(), - backup.Status.DatabaseInstance, - getEndTime(backup.Status.Process), - getProcessStatus(backup.Status.Process), - duration.HumanDuration(time.Since(backup.GetCreationTimestamp().Time))}, - Object: runtime.RawExtension{Object: backup}, - } + return apiserver.GetBackupTable( + backup.GetName(), + backup.Status.DatabaseInstance, + getProcessStatus(backup.Status.Process), + duration.HumanDuration(time.Since(backup.GetCreationTimestamp().Time)), + getStartTime(backup.Status.Process), + getEndTime(backup.Status.Process), + backup) } func getEndTime(process *runtime.RawExtension) string { @@ -75,6 +68,17 @@ func getEndTime(process *runtime.RawExtension) string { return "" } +func getStartTime(process *runtime.RawExtension) string { + if process != nil && process.Object != nil { + if v, err := runtime.DefaultUnstructuredConverter.ToUnstructured(process.Object); err == nil { + if startTime, exists, _ := unstructured.NestedString(v, v1.Timing, v1.Start); exists { + return startTime + } + } + } + return "" +} + func getProcessStatus(process *runtime.RawExtension) string { if process != nil && process.Object != nil { unstructuredProcess, err := runtime.DefaultUnstructuredConverter.ToUnstructured(process.Object) diff --git a/pkg/apiserver/vshn/redis/table.go b/pkg/apiserver/vshn/redis/table.go index 9d6c1e7..f9d313f 100644 --- a/pkg/apiserver/vshn/redis/table.go +++ b/pkg/apiserver/vshn/redis/table.go @@ -3,6 +3,9 @@ package redis import ( "context" "fmt" + "github.com/vshn/appcat-apiserver/pkg/apiserver" + "k8s.io/apimachinery/pkg/util/duration" + "time" appcatv1 "github.com/vshn/appcat-apiserver/apis/appcat/v1" "k8s.io/apimachinery/pkg/api/meta" @@ -37,23 +40,21 @@ func (v *vshnRedisBackupStorage) ConvertToTable(_ context.Context, obj runtime.O } if opt, ok := tableOptions.(*metav1.TableOptions); !ok || !opt.NoHeaders { - table.ColumnDefinitions = []metav1.TableColumnDefinition{ - {Name: "Backup ID", Type: "string", Format: "name", Description: "ID of the snapshot"}, - {Name: "Database Instance", Type: "string", Description: "The redis instance"}, - {Name: "Backup Time", Type: "string", Description: "When backup was made"}, - } + table.ColumnDefinitions = apiserver.GetBackupColumnDefinition() } return table, nil } +// ToDo Once k8up exposes start time, update the code here func backupToTableRow(backup *appcatv1.VSHNRedisBackup) metav1.TableRow { - - return metav1.TableRow{ - Cells: []interface{}{ - trimStringLength(backup.Status.ID), - backup.Status.Instance, - backup.Status.Date}, - Object: runtime.RawExtension{Object: backup}, - } + return apiserver.GetBackupTable( + trimStringLength(backup.Status.ID), + backup.Status.Instance, + "Completed", + duration.HumanDuration(time.Since(backup.GetCreationTimestamp().Time)), + backup.Status.Date.Format(time.RFC3339), + backup.Status.Date.Format(time.RFC3339), + backup, + ) }