package vm import ( "context" "database/sql" "errors" "fmt" ) type SQLRepository struct { db *sql.DB } func NewSQLRepository(db *sql.DB) SQLRepository { return SQLRepository{db: db} } func (r SQLRepository) ListProjectVMs(ctx context.Context, profileID string, projectID string) ([]VM, bool, error) { rows, err := r.db.QueryContext(ctx, ` select v.id::text, v.project_id::text, p.tenant_id::text, v.cluster_id::text, v.proxmox_vmid, v.node, v.name, v.status::text, v.vcpu, v.ram_mb, v.disk_gb, v.created_at, v.updated_at, p.name, m.role::text from public.projects p join public.memberships m on m.tenant_id = p.tenant_id and m.profile_id = $1 left join public.vms v on v.project_id = p.id where p.id = $2 order by v.created_at desc nulls last, v.name asc `, profileID, projectID) if err != nil { return nil, false, err } defer rows.Close() var vms []VM foundProject := false for rows.Next() { var record nullableVMRecord if err := rows.Scan( &record.ID, &record.ProjectID, &record.TenantID, &record.ClusterID, &record.ProxmoxVMID, &record.Node, &record.Name, &record.Status, &record.VCPU, &record.RAMMB, &record.DiskGB, &record.CreatedAt, &record.UpdatedAt, &record.ProjectName, &record.MembershipRole, ); err != nil { return nil, false, err } foundProject = true if !record.ID.Valid { continue } vms = append(vms, record.vm()) } if err := rows.Err(); err != nil { return nil, false, err } return vms, foundProject, nil } func (r SQLRepository) GetVM(ctx context.Context, profileID string, vmID string) (VM, bool, error) { var vm VM err := r.db.QueryRowContext(ctx, ` select v.id::text, v.project_id::text, p.tenant_id::text, v.cluster_id::text, v.proxmox_vmid, v.node, v.name, v.status::text, v.vcpu, v.ram_mb, v.disk_gb, v.created_at, v.updated_at, p.name, m.role::text from public.vms v join public.projects p on p.id = v.project_id join public.memberships m on m.tenant_id = p.tenant_id and m.profile_id = $1 where v.id = $2 `, profileID, vmID).Scan( &vm.ID, &vm.ProjectID, &vm.TenantID, &vm.ClusterID, &vm.ProxmoxVMID, &vm.Node, &vm.Name, &vm.Status, &vm.VCPU, &vm.RAMMB, &vm.DiskGB, &vm.CreatedAt, &vm.UpdatedAt, &vm.ProjectName, &vm.MembershipRole, ) if errors.Is(err, sql.ErrNoRows) { return VM{}, false, nil } if err != nil { return VM{}, false, err } return vm, true, nil } type nullableVMRecord struct { ID sql.NullString ProjectID sql.NullString TenantID sql.NullString ClusterID sql.NullString ProxmoxVMID sql.NullInt64 Node sql.NullString Name sql.NullString Status sql.NullString VCPU sql.NullInt64 RAMMB sql.NullInt64 DiskGB sql.NullInt64 CreatedAt sql.NullTime UpdatedAt sql.NullTime ProjectName string MembershipRole string } func (r nullableVMRecord) vm() VM { return VM{ ID: r.ID.String, ProjectID: r.ProjectID.String, TenantID: r.TenantID.String, ClusterID: r.ClusterID.String, ProxmoxVMID: int(r.ProxmoxVMID.Int64), Node: r.Node.String, Name: r.Name.String, Status: r.Status.String, VCPU: int(r.VCPU.Int64), RAMMB: int(r.RAMMB.Int64), DiskGB: int(r.DiskGB.Int64), CreatedAt: r.CreatedAt.Time, UpdatedAt: r.UpdatedAt.Time, ProjectName: r.ProjectName, MembershipRole: r.MembershipRole, } } type ProjectInfo struct { TenantID string Name string Role string } type QuotaInfo struct { UsedVMs int UsedVCPU int UsedRAMMB int UsedDiskGB int MaxVMs int MaxVCPU int MaxRAMMB int MaxDiskGB int } func (r SQLRepository) GetProjectInfo(ctx context.Context, profileID string, projectID string) (ProjectInfo, bool, error) { var info ProjectInfo err := r.db.QueryRowContext(ctx, ` select p.tenant_id::text, p.name, m.role::text from public.projects p join public.memberships m on m.tenant_id = p.tenant_id and m.profile_id = $1 where p.id = $2 `, profileID, projectID).Scan(&info.TenantID, &info.Name, &info.Role) if errors.Is(err, sql.ErrNoRows) { return ProjectInfo{}, false, nil } if err != nil { return ProjectInfo{}, false, err } return info, true, nil } func (r SQLRepository) CheckQuota(ctx context.Context, projectID string) (QuotaInfo, error) { var q QuotaInfo err := r.db.QueryRowContext(ctx, ` select coalesce(sum(case when v.status <> 'failed' and v.status <> 'deleting' and v.status <> 'deleted' then 1 else 0 end), 0) as used_vms, coalesce(sum(case when v.status <> 'failed' and v.status <> 'deleting' and v.status <> 'deleted' then v.vcpu else 0 end), 0) as used_vcpu, coalesce(sum(case when v.status <> 'failed' and v.status <> 'deleting' and v.status <> 'deleted' then v.ram_mb else 0 end), 0) as used_ram_mb, coalesce(sum(case when v.status <> 'failed' and v.status <> 'deleting' and v.status <> 'deleted' then v.disk_gb else 0 end), 0) as used_disk_gb, coalesce(pq.max_vms, 10), coalesce(pq.max_vcpu, 8), coalesce(pq.max_ram_mb, 16384), coalesce(pq.max_disk_gb, 200) from public.projects p left join public.vms v on v.project_id = p.id left join public.project_quotas pq on pq.project_id = p.id where p.id = $1 group by pq.max_vms, pq.max_vcpu, pq.max_ram_mb, pq.max_disk_gb `, projectID).Scan(&q.UsedVMs, &q.UsedVCPU, &q.UsedRAMMB, &q.UsedDiskGB, &q.MaxVMs, &q.MaxVCPU, &q.MaxRAMMB, &q.MaxDiskGB) if errors.Is(err, sql.ErrNoRows) { q.MaxVMs = 10 q.MaxVCPU = 8 q.MaxRAMMB = 16384 q.MaxDiskGB = 200 return q, nil } if err != nil { return QuotaInfo{}, err } return q, nil } func (r SQLRepository) ReserveNextVMID(ctx context.Context, clusterID string) (int, error) { var vmid int err := r.db.QueryRowContext(ctx, ` select public.reserve_next_vmid($1::uuid) `, clusterID).Scan(&vmid) if err != nil { return 0, fmt.Errorf("reserve vmid: %w", err) } return vmid, nil } func (r SQLRepository) InsertVM(ctx context.Context, vm InsertVMRecord) (VM, error) { var result VM err := r.db.QueryRowContext(ctx, ` insert into public.vms (project_id, cluster_id, proxmox_vmid, node, name, status, vcpu, ram_mb, disk_gb) values ($1, $2, $3, $4, $5, $6, $7, $8, $9) returning id::text, project_id::text, cluster_id::text, proxmox_vmid, node, name, status::text, vcpu, ram_mb, disk_gb, created_at, updated_at `, vm.ProjectID, vm.ClusterID, vm.ProxmoxVMID, vm.Node, vm.Name, vm.Status, vm.VCPU, vm.RAMMB, vm.DiskGB).Scan( &result.ID, &result.ProjectID, &result.ClusterID, &result.ProxmoxVMID, &result.Node, &result.Name, &result.Status, &result.VCPU, &result.RAMMB, &result.DiskGB, &result.CreatedAt, &result.UpdatedAt, ) if err != nil { return VM{}, fmt.Errorf("insert vm: %w", err) } return result, nil } type InsertVMRecord struct { ProjectID string ClusterID string ProxmoxVMID int Node string Name string Status string VCPU int RAMMB int DiskGB int }