Initial Phoenix Sankofa Cloud setup
- Complete project structure with Next.js frontend - GraphQL API backend with Apollo Server - Portal application with NextAuth - Crossplane Proxmox provider - GitOps configurations - CI/CD pipelines - Testing infrastructure (Vitest, Jest, Go tests) - Error handling and monitoring - Security hardening - UI component library - Documentation
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
package virtualmachine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
proxmoxv1alpha1 "github.com/yourorg/crossplane-provider-proxmox/apis/v1alpha1"
|
||||
"github.com/yourorg/crossplane-provider-proxmox/pkg/proxmox"
|
||||
)
|
||||
|
||||
// ProxmoxVMReconciler reconciles a ProxmoxVM object
|
||||
type ProxmoxVMReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=proxmox.yourorg.io,resources=proxmoxvms,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=proxmox.yourorg.io,resources=proxmoxvms/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=proxmox.yourorg.io,resources=proxmoxvms/finalizers,verbs=update
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop
|
||||
func (r *ProxmoxVMReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
var vm proxmoxv1alpha1.ProxmoxVM
|
||||
if err := r.Get(ctx, req.NamespacedName, &vm); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
// Get ProviderConfig
|
||||
var providerConfig proxmoxv1alpha1.ProviderConfig
|
||||
providerConfigName := vm.Spec.ProviderConfigReference.Name
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: providerConfigName}, &providerConfig); err != nil {
|
||||
return ctrl.Result{}, errors.Wrapf(err, "cannot get provider config %s", providerConfigName)
|
||||
}
|
||||
|
||||
// Get credentials from secret
|
||||
creds, err := r.getCredentials(ctx, &providerConfig)
|
||||
if err != nil {
|
||||
logger.Error(err, "cannot get credentials")
|
||||
return ctrl.Result{RequeueAfter: 30 * time.Second}, errors.Wrap(err, "cannot get credentials")
|
||||
}
|
||||
|
||||
// Find the site configuration
|
||||
site, err := r.findSite(&providerConfig, vm.Spec.ForProvider.Site)
|
||||
if err != nil {
|
||||
logger.Error(err, "cannot find site", "site", vm.Spec.ForProvider.Site)
|
||||
return ctrl.Result{RequeueAfter: 30 * time.Second}, errors.Wrapf(err, "cannot find site %s", vm.Spec.ForProvider.Site)
|
||||
}
|
||||
|
||||
// Create Proxmox client
|
||||
proxmoxClient, err := proxmox.NewClient(
|
||||
site.Endpoint,
|
||||
creds.Username,
|
||||
creds.Password,
|
||||
site.InsecureSkipTLSVerify,
|
||||
)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, errors.Wrap(err, "cannot create Proxmox client")
|
||||
}
|
||||
|
||||
// Reconcile VM
|
||||
if vm.Status.VMID == 0 {
|
||||
// Create VM
|
||||
logger.Info("Creating VM", "name", vm.Name, "node", vm.Spec.ForProvider.Node)
|
||||
|
||||
vmConfig := proxmox.VMConfig{
|
||||
Name: vm.Spec.ForProvider.Name,
|
||||
CPU: vm.Spec.ForProvider.CPU,
|
||||
Memory: vm.Spec.ForProvider.Memory,
|
||||
Disk: vm.Spec.ForProvider.Disk,
|
||||
Storage: vm.Spec.ForProvider.Storage,
|
||||
Network: vm.Spec.ForProvider.Network,
|
||||
Image: vm.Spec.ForProvider.Image,
|
||||
UserData: vm.Spec.ForProvider.UserData,
|
||||
SSHKeys: vm.Spec.ForProvider.SSHKeys,
|
||||
}
|
||||
|
||||
createdVM, err := proxmoxClient.CreateVM(vm.Spec.ForProvider.Node, vmConfig)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, errors.Wrap(err, "cannot create VM")
|
||||
}
|
||||
|
||||
vm.Status.VMID = createdVM.ID
|
||||
vm.Status.State = createdVM.Status
|
||||
vm.Status.IPAddress = createdVM.IPAddress
|
||||
|
||||
if err := r.Status().Update(ctx, &vm); err != nil {
|
||||
return ctrl.Result{}, errors.Wrap(err, "cannot update VM status")
|
||||
}
|
||||
|
||||
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
|
||||
}
|
||||
|
||||
// Update VM if needed
|
||||
currentVM, err := proxmoxClient.GetVM(vm.Spec.ForProvider.Node, vm.Status.VMID)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, errors.Wrap(err, "cannot get VM")
|
||||
}
|
||||
|
||||
// Update status
|
||||
vm.Status.State = currentVM.Status
|
||||
vm.Status.IPAddress = currentVM.IPAddress
|
||||
|
||||
if err := r.Status().Update(ctx, &vm); err != nil {
|
||||
return ctrl.Result{}, errors.Wrap(err, "cannot update VM status")
|
||||
}
|
||||
|
||||
// Check if VM needs to be updated
|
||||
needsUpdate := false
|
||||
if vm.Spec.ForProvider.CPU != 0 && currentVM.Config.CPU != vm.Spec.ForProvider.CPU {
|
||||
needsUpdate = true
|
||||
}
|
||||
if vm.Spec.ForProvider.Memory != "" && currentVM.Config.Memory != vm.Spec.ForProvider.Memory {
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if needsUpdate {
|
||||
logger.Info("Updating VM", "name", vm.Name, "vmId", vm.Status.VMID)
|
||||
|
||||
vmConfig := proxmox.VMConfig{
|
||||
CPU: vm.Spec.ForProvider.CPU,
|
||||
Memory: vm.Spec.ForProvider.Memory,
|
||||
}
|
||||
|
||||
if err := proxmoxClient.UpdateVM(vm.Spec.ForProvider.Node, vm.Status.VMID, vmConfig); err != nil {
|
||||
return ctrl.Result{}, errors.Wrap(err, "cannot update VM")
|
||||
}
|
||||
|
||||
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
|
||||
}
|
||||
|
||||
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager
|
||||
func (r *ProxmoxVMReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&proxmoxv1alpha1.ProxmoxVM{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
type credentials struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (r *ProxmoxVMReconciler) getCredentials(ctx context.Context, config *proxmoxv1alpha1.ProviderConfig) (*credentials, error) {
|
||||
if config.Spec.Credentials.SecretRef == nil {
|
||||
return nil, fmt.Errorf("no secret reference in provider config")
|
||||
}
|
||||
|
||||
secretRef := config.Spec.Credentials.SecretRef
|
||||
|
||||
// In a real implementation, you would:
|
||||
// 1. Get the secret from Kubernetes
|
||||
// 2. Parse the credentials (JSON, username/password, etc.)
|
||||
// 3. Return the credentials
|
||||
|
||||
// This is a placeholder
|
||||
return &credentials{
|
||||
Username: "root@pam",
|
||||
Password: "placeholder",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *ProxmoxVMReconciler) findSite(config *proxmoxv1alpha1.ProviderConfig, siteName string) (*proxmoxv1alpha1.ProxmoxSite, error) {
|
||||
for _, site := range config.Spec.Sites {
|
||||
if site.Name == siteName {
|
||||
return &site, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("site %s not found", siteName)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package virtualmachine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
|
||||
proxmoxv1alpha1 "github.com/yourorg/crossplane-provider-proxmox/apis/v1alpha1"
|
||||
)
|
||||
|
||||
func TestProxmoxVMReconciler_Reconcile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
vm *proxmoxv1alpha1.ProxmoxVM
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid VM",
|
||||
vm: &proxmoxv1alpha1.ProxmoxVM{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-vm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: proxmoxv1alpha1.ProxmoxVMSpec{
|
||||
ForProvider: proxmoxv1alpha1.ProxmoxVMParameters{
|
||||
Node: "pve1",
|
||||
Name: "test-vm",
|
||||
CPU: 2,
|
||||
Memory: "4Gi",
|
||||
Disk: "50Gi",
|
||||
Storage: "local-lvm",
|
||||
Network: "vmbr0",
|
||||
Image: "ubuntu-22.04-cloud",
|
||||
Site: "us-east-1",
|
||||
},
|
||||
ProviderConfigReference: proxmoxv1alpha1.ProviderConfigReference{
|
||||
Name: "test-provider-config",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = proxmoxv1alpha1.AddToScheme(scheme)
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(tt.vm).
|
||||
Build()
|
||||
|
||||
r := &ProxmoxVMReconciler{
|
||||
Client: fakeClient,
|
||||
Scheme: scheme,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
req := ctrl.Request{
|
||||
NamespacedName: client.ObjectKeyFromObject(tt.vm),
|
||||
}
|
||||
|
||||
_, err := r.Reconcile(ctx, req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Reconcile() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxmoxVMReconciler_getCredentials(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
providerConfig *proxmoxv1alpha1.ProviderConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing secret reference",
|
||||
providerConfig: &proxmoxv1alpha1.ProviderConfig{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-config",
|
||||
},
|
||||
Spec: proxmoxv1alpha1.ProviderConfigSpec{
|
||||
Credentials: proxmoxv1alpha1.ProviderCredentials{
|
||||
Source: proxmoxv1alpha1.CredentialsSourceSecret,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = proxmoxv1alpha1.AddToScheme(scheme)
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
Build()
|
||||
|
||||
r := &ProxmoxVMReconciler{
|
||||
Client: fakeClient,
|
||||
Scheme: scheme,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := r.getCredentials(ctx, tt.providerConfig)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getCredentials() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user