Переглянути джерело

Merge branch 'main' of github.com:loicblt/byop-engine

loic boulet 1 місяць тому
батько
коміт
80466a988b

+ 3 - 1
.gitignore

@@ -1,2 +1,4 @@
 config.yaml
-.garbage/
+.garbage/
+ovh.conf
+data/

+ 44 - 0
Dockerfile

@@ -0,0 +1,44 @@
+FROM golang:1.24.2-alpine AS builder
+
+WORKDIR /app
+
+# Install necessary build tools and SQLite dependencies
+RUN apk add --no-cache git make build-base sqlite-dev
+
+# Copy go mod files first for better caching
+COPY go.mod go.sum ./
+
+# Download dependencies
+RUN go mod download
+
+# Copy the rest of the source code
+COPY . .
+
+# Build the application with SQLite support
+RUN go build -tags 'sqlite' -o byop-engine .
+
+# Create a minimal production image
+FROM alpine:3.17
+
+WORKDIR /app
+
+# Install runtime dependencies including SQLite
+RUN apk add --no-cache ca-certificates tzdata sqlite
+
+# Create data directory for SQLite
+RUN mkdir -p /app/data && chmod 755 /app/data
+
+# Copy the binary from the builder stage
+COPY --from=builder /app/byop-engine /app/byop-engine
+COPY --from=builder /app/config.sample.yml /app/config.yaml
+
+# Create a non-root user and switch to it
+RUN adduser -D -u 1000 byopuser && \
+    chown -R byopuser:byopuser /app
+USER byopuser
+
+# Expose the application port
+EXPOSE 8080
+
+# Run the application
+CMD ["/app/byop-engine"]

+ 9 - 12
app/init.go

@@ -60,8 +60,8 @@ func (a *App) initCommonServices() error {
 		&models.Component{}, // Renamed from App
 		&models.App{},       // Renamed from Template
 		&models.Deployment{},
-		&models.DeployedApp{},
-		&models.DeployedAppResource{},
+		&models.DeployedComponent{},
+		&models.DeployedComponentResource{},
 		// Add other models here
 	); err != nil {
 		return fmt.Errorf("migrate database: %w", err)
@@ -139,20 +139,17 @@ func (a *App) initHandlers() error {
 	return nil
 }
 
+// Updated loadProviders method
 func (a *App) loadProviders() error {
-	for name, config := range a.cnf.Providers {
-		provider, ok := cloud.GetProvider(name)
-		if !ok {
-			return fmt.Errorf("provider %s not found", name)
-		}
-		err := provider.Initialize(config)
+	for name, cnf := range a.cnf.Providers {
+		// Use the new InitializeProvider function instead of GetProvider + Initialize
+		err := cloud.InitializeProvider(name, cnf)
 		if err != nil {
 			return fmt.Errorf("initialize provider %s: %w", name, err)
 		}
 		a.entry.WithField("provider", name).Info("Provider initialized")
 	}
 	a.entry.Info("All providers loaded successfully")
-
 	return nil
 }
 
@@ -203,7 +200,7 @@ func (a *App) setupRoutes() {
 	providers := protected.Group("/providers")
 	a.providerHandler.RegisterRoutes(providers)
 
-	// Client routes
+	// Client routes - registering both with and without trailing slash
 	clients := protected.Group("/clients")
 	clients.GET("", a.clientModule.Handler.ListClients)
 	clients.POST("", a.clientModule.Handler.CreateClient)
@@ -212,7 +209,7 @@ func (a *App) setupRoutes() {
 	clients.DELETE("/:id", a.clientModule.Handler.DeleteClient)
 	clients.GET("/:id/deployments", a.clientModule.Handler.GetClientDeployments)
 
-	// User routes
+	// User routes - registering both with and without trailing slash
 	users := protected.Group("/users")
 	users.GET("", a.userModule.Handler.ListUsers)
 	users.GET("/:id", a.userModule.Handler.GetUser)
@@ -238,7 +235,7 @@ func (a *App) setupRoutes() {
 	apps.DELETE("/:id", a.appModule.Handler.DeleteApp)
 	apps.GET("/:id/deployments", a.appModule.Handler.GetAppDeployments)
 
-	// Deployment routes
+	// Deployment routes - need to handle both versions
 	deployments := protected.Group("/deployments")
 	deployments.GET("", a.deploymentModule.Handler.ListDeployments)
 	deployments.POST("", a.deploymentModule.Handler.CreateDeployment)

+ 11 - 0
app/server.go

@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"os"
 	"os/signal"
+	"sync"
 	"syscall"
 	"time"
 
@@ -45,6 +46,8 @@ type App struct {
 	// Resource Handlers
 	providerHandler *handlers.ProviderHandler
 	// ticketHandler     *handlers.TicketHandler
+	stopped bool
+	wg      sync.WaitGroup
 	// monitoringHandler *handlers.MonitoringHandler
 }
 
@@ -97,8 +100,16 @@ func NewApp(cnf *config.Config) (*App, error) {
 		gin.SetMode(gin.ReleaseMode)
 	}
 	app.rtr = gin.New()
+
+	// Disable automatic redirection of trailing slashes
+	// This prevents 301 redirects that can cause CORS issues
+	app.rtr.RedirectTrailingSlash = false
+	app.rtr.RedirectFixedPath = false
+
 	app.rtr.Use(gin.Recovery())
 	app.rtr.Use(mw.Logger)
+	// Add CORS middleware to handle cross-origin requests
+	app.rtr.Use(mw.CORS())
 
 	// Initialize services and handlers
 	if err := app.initCommonServices(); err != nil {

+ 2 - 2
auth/auth.go

@@ -24,8 +24,8 @@ type Service interface {
 	// GenerateToken creates new access and refresh tokens for a user
 	GenerateToken(ctx context.Context, clientID string, role string) (*TokenResponse, error)
 
-	// ValidateToken verifies a token and returns the client ID if valid
-	ValidateToken(ctx context.Context, token string) (string, error)
+	// ValidateToken verifies a token and returns the client ID and role if valid
+	ValidateToken(ctx context.Context, token string) (clientID string, role string, err error)
 
 	// RefreshToken creates a new access token based on a valid refresh token
 	RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error)

+ 7 - 7
auth/jwt.go

@@ -103,29 +103,29 @@ func (s *JWTService) GenerateToken(ctx context.Context, clientID string, role st
 	}, nil
 }
 
-// ValidateToken validates a JWT token and returns the client ID if valid
-func (s *JWTService) ValidateToken(ctx context.Context, tokenString string) (string, error) {
+// ValidateToken validates a JWT token and returns the client ID and role if valid
+func (s *JWTService) ValidateToken(ctx context.Context, tokenString string) (string, string, error) {
 	// Check if the token is blacklisted
 	isBlacklisted, err := s.tokenStore.IsBlacklisted(ctx, tokenString)
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	if isBlacklisted {
-		return "", ErrTokenBlacklisted
+		return "", "", ErrTokenBlacklisted
 	}
 
 	// Parse and validate the token
 	claims, err := s.parseToken(tokenString)
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 
 	// For validation purposes, we only accept access tokens
 	if claims.Type != AccessToken {
-		return "", errors.New("invalid token type")
+		return "", "", errors.New("invalid token type")
 	}
 
-	return claims.ClientID, nil
+	return claims.ClientID, claims.Role, nil
 }
 
 // parseToken parses and validates a JWT token and returns the claims if valid


+ 164 - 460
cloud/ovh.go

@@ -4,22 +4,28 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"strconv"
+	"strings"
 	"time"
 
+	"git.linuxforward.com/byop/byop-engine/models"
 	"github.com/ovh/go-ovh/ovh"
+	"github.com/sirupsen/logrus"
 )
 
 // OVHProvider implements the Provider interface for OVH Cloud
 type OVHProvider struct {
+	entry      *logrus.Entry
 	client     *ovh.Client
-	projectID  string
 	region     string
 	configured bool
 }
 
 // NewOVHProvider creates a new OVH provider
 func NewOVHProvider() Provider {
-	return &OVHProvider{}
+	return &OVHProvider{
+		entry: logrus.WithField("provider", "ovh"),
+	}
 }
 
 func init() {
@@ -29,7 +35,7 @@ func init() {
 // Initialize sets up the OVH provider with credentials and configuration
 func (p *OVHProvider) Initialize(config map[string]string) error {
 	// Check required configuration
-	requiredKeys := []string{"application_key", "application_secret", "consumer_key", "project_id"}
+	requiredKeys := []string{"client_id", "client_secret", "endpoint"}
 	for _, key := range requiredKeys {
 		if _, ok := config[key]; !ok {
 			return fmt.Errorf("missing required configuration key: %s", key)
@@ -37,44 +43,32 @@ func (p *OVHProvider) Initialize(config map[string]string) error {
 	}
 
 	// Create OVH client
-	client, err := ovh.NewClient(
-		"ovh-eu", // Endpoint (can be configurable)
-		config["application_key"],
-		config["application_secret"],
-		config["consumer_key"],
+	client, err := ovh.NewOAuth2Client(
+		config["endpoint"],
+		config["client_id"],
+		config["client_secret"],
 	)
 	if err != nil {
 		return fmt.Errorf("failed to create OVH client: %w", err)
 	}
 
-	p.client = client
-	p.projectID = config["project_id"]
-
-	// Set default region if provided
-	if region, ok := config["region"]; ok {
-		p.region = region
+	// Test the client by trying to list VPS IDs
+	var vpsIDs []string
+	if err = client.Get("/vps", &vpsIDs); err != nil {
+		return fmt.Errorf("failed to connect to OVH API: %w", err)
 	}
 
+	// Set client before testing VPS connectivity
+	p.client = client
 	p.configured = true
-	return nil
-}
-
-// Validate checks if the OVH provider credentials are valid
-func (p *OVHProvider) Validate(ctx context.Context) (bool, error) {
-	if !p.configured {
-		return false, errors.New("provider not configured")
+	// Set region if provided
+	if region, ok := config["region"]; ok {
+		p.region = region
 	}
 
-	// Try to get project info to verify credentials
-	path := fmt.Sprintf("/cloud/project/%s", p.projectID)
-	var project map[string]interface{}
-
-	err := p.client.Get(path, &project)
-	if err != nil {
-		return false, fmt.Errorf("validation failed: %w", err)
-	}
+	p.entry.Info("OVH provider initialized")
 
-	return true, nil
+	return nil
 }
 
 // ListRegions lists all available OVH regions
@@ -83,81 +77,51 @@ func (p *OVHProvider) ListRegions(ctx context.Context) ([]Region, error) {
 		return nil, errors.New("provider not configured")
 	}
 
-	path := fmt.Sprintf("/cloud/project/%s/region", p.projectID)
-	var regionIDs []string
-
-	err := p.client.Get(path, &regionIDs)
+	type OVHRegion struct {
+		AvailabilityZones []string `json:"availabilityZones"`
+		CardinalPoint     string   `json:"cardinalPoint"`
+		CityCode          string   `json:"cityCode"`
+		CityLatitude      float64  `json:"cityLatitude"`
+		CityLongitude     float64  `json:"cityLongitude"`
+		CityName          string   `json:"cityName"`
+		Code              string   `json:"code"`
+		CountryCode       string   `json:"countryCode"`
+		CountryName       string   `json:"countryName"`
+		GeographyCode     string   `json:"geographyCode"`
+		GeographyName     string   `json:"geographyName"`
+		Location          string   `json:"location"`
+		Name              string   `json:"name"`
+		OpeningYear       int      `json:"openingYear"`
+		SpecificType      string   `json:"specificType"`
+		Type              string   `json:"type"`
+	}
+
+	// Get the list of regions from OVH
+	path := "/v2/location"
+	var regions []Region
+	var ovhRegions []OVHRegion
+	err := p.client.Get(path, &ovhRegions)
 	if err != nil {
 		return nil, fmt.Errorf("failed to list regions: %w", err)
 	}
 
-	regions := make([]Region, 0, len(regionIDs))
-	for _, id := range regionIDs {
-		var regionDetails struct {
-			Name          string `json:"name"`
-			ContinentCode string `json:"continentCode"`
-			Status        string `json:"status"`
-		}
-
-		regionPath := fmt.Sprintf("/cloud/project/%s/region/%s", p.projectID, id)
-		err := p.client.Get(regionPath, &regionDetails)
-		if err != nil {
-			continue // Skip this region if we can't get details
-		}
+	fmt.Printf("OVH regions: %v\n", ovhRegions)
 
+	// Convert OVHRegion to Region
+	for _, ovhRegion := range ovhRegions {
 		regions = append(regions, Region{
-			ID:   id,
-			Name: regionDetails.Name,
-			Zone: regionDetails.ContinentCode,
+			ID:   ovhRegion.Name,
+			Zone: strings.Join(ovhRegion.AvailabilityZones, ","),
+			// Add other fields as needed
 		})
 	}
-
 	return regions, nil
 }
 
 // ListInstanceSizes lists available VM sizes (flavors) in OVH
 func (p *OVHProvider) ListInstanceSizes(ctx context.Context, region string) ([]InstanceSize, error) {
-	if !p.configured {
-		return nil, errors.New("provider not configured")
-	}
-
-	if region == "" {
-		region = p.region
-	}
-
-	if region == "" {
-		return nil, errors.New("region must be specified")
-	}
-
-	path := fmt.Sprintf("/cloud/project/%s/flavor?region=%s", p.projectID, region)
-	var flavors []struct {
-		ID          string  `json:"id"`
-		Name        string  `json:"name"`
-		Vcpus       int     `json:"vcpus"`
-		RAM         int     `json:"ram"`  // in MB
-		Disk        int     `json:"disk"` // in GB
-		Type        string  `json:"type"`
-		HourlyPrice float64 `json:"hourlyPrice"`
-	}
-
-	err := p.client.Get(path, &flavors)
-	if err != nil {
-		return nil, fmt.Errorf("failed to list flavors: %w", err)
-	}
-
-	sizes := make([]InstanceSize, 0, len(flavors))
-	for _, flavor := range flavors {
-		sizes = append(sizes, InstanceSize{
-			ID:       flavor.ID,
-			Name:     flavor.Name,
-			CPUCores: flavor.Vcpus,
-			MemoryGB: flavor.RAM / 1024, // Convert MB to GB
-			DiskGB:   flavor.Disk,
-			Price:    flavor.HourlyPrice,
-		})
-	}
-
-	return sizes, nil
+	// TODO: Implement this method
+	return nil, errors.New("not implemented")
 }
 
 // ListInstances lists all instances in OVH
@@ -166,407 +130,147 @@ func (p *OVHProvider) ListInstances(ctx context.Context) ([]Instance, error) {
 		return nil, errors.New("provider not configured")
 	}
 
-	path := fmt.Sprintf("/cloud/project/%s/instance", p.projectID)
-	var ovhInstances []struct {
-		ID          string    `json:"id"`
-		Name        string    `json:"name"`
-		Status      string    `json:"status"`
-		Created     time.Time `json:"created"`
-		Region      string    `json:"region"`
-		FlavorID    string    `json:"flavorId"`
-		ImageID     string    `json:"imageId"`
-		IPAddresses []struct {
-			IP      string `json:"ip"`
-			Type    string `json:"type"`    // public or private
-			Version int    `json:"version"` // 4 or 6
-		} `json:"ipAddresses"`
-	}
-
-	err := p.client.Get(path, &ovhInstances)
+	path := "/vps"
+	var vpsIDs []string
+	var vpsList []*models.OVHVPS
+	err := p.client.Get(path, &vpsIDs)
 	if err != nil {
 		return nil, fmt.Errorf("failed to list instances: %w", err)
 	}
 
-	instances := make([]Instance, 0, len(ovhInstances))
-	for _, ovhInstance := range ovhInstances {
-		instance := Instance{
-			ID:        ovhInstance.ID,
-			Name:      ovhInstance.Name,
-			Region:    ovhInstance.Region,
-			Size:      ovhInstance.FlavorID,
-			ImageID:   ovhInstance.ImageID,
-			Status:    mapOVHStatus(ovhInstance.Status),
-			CreatedAt: ovhInstance.Created,
+	// Get details for each VPS ID
+	for _, vpsID := range vpsIDs {
+		path := fmt.Sprintf("/vps/%s", vpsID)
+		vps := &models.OVHVPS{}
+		err := p.client.Get(path, vps)
+		if err != nil {
+			return nil, fmt.Errorf("failed to get instance %s: %w", vpsID, err)
 		}
-
-		// Extract IP addresses
-		for _, ip := range ovhInstance.IPAddresses {
-			if ip.Version == 4 { // Only use IPv4 for now
-				if ip.Type == "public" {
-					instance.IPAddress = ip.IP
-				} else {
-					instance.PrivateIP = ip.IP
-				}
-			}
+		vpsList = append(vpsList, vps)
+	}
+
+	instances := make([]Instance, len(vpsList))
+	for i, vps := range vpsList {
+		// convert size
+		instances[i] = Instance{
+			ID:     vps.Name,
+			Name:   vps.DisplayName,
+			Region: vps.Zone,
+			Size:   strconv.Itoa(vps.VCore),
+			Status: vps.State,
 		}
-
-		instances = append(instances, instance)
 	}
 
 	return instances, nil
 }
 
-// GetInstance gets a specific instance by ID
-func (p *OVHProvider) GetInstance(ctx context.Context, id string) (*Instance, error) {
-	if !p.configured {
-		return nil, errors.New("provider not configured")
-	}
-
-	path := fmt.Sprintf("/cloud/project/%s/instance/%s", p.projectID, id)
-	var ovhInstance struct {
-		ID          string    `json:"id"`
-		Name        string    `json:"name"`
-		Status      string    `json:"status"`
-		Created     time.Time `json:"created"`
-		Region      string    `json:"region"`
-		FlavorID    string    `json:"flavorId"`
-		ImageID     string    `json:"imageId"`
-		IPAddresses []struct {
-			IP      string `json:"ip"`
-			Type    string `json:"type"`    // public or private
-			Version int    `json:"version"` // 4 or 6
-		} `json:"ipAddresses"`
-	}
-
-	err := p.client.Get(path, &ovhInstance)
-	if err != nil {
-		return nil, fmt.Errorf("failed to get instance: %w", err)
-	}
-
-	instance := &Instance{
-		ID:        ovhInstance.ID,
-		Name:      ovhInstance.Name,
-		Region:    ovhInstance.Region,
-		Size:      ovhInstance.FlavorID,
-		ImageID:   ovhInstance.ImageID,
-		Status:    mapOVHStatus(ovhInstance.Status),
-		CreatedAt: ovhInstance.Created,
-	}
-
-	// Extract IP addresses
-	for _, ip := range ovhInstance.IPAddresses {
-		if ip.Version == 4 { // Only use IPv4 for now
-			if ip.Type == "public" {
-				instance.IPAddress = ip.IP
-			} else {
-				instance.PrivateIP = ip.IP
-			}
-		}
-	}
-
-	return instance, nil
-}
-
-// CreateInstance creates a new instance in OVH
-func (p *OVHProvider) CreateInstance(ctx context.Context, opts InstanceCreateOpts) (*Instance, error) {
-	if !p.configured {
-		return nil, errors.New("provider not configured")
-	}
-
-	// Prepare create request
-	path := fmt.Sprintf("/cloud/project/%s/instance", p.projectID)
-	request := struct {
-		Name     string   `json:"name"`
-		Region   string   `json:"region"`
-		FlavorID string   `json:"flavorId"`
-		ImageID  string   `json:"imageId"`
-		SSHKeyID []string `json:"sshKeyId,omitempty"`
-		UserData string   `json:"userData,omitempty"`
-		Networks []string `json:"networks,omitempty"`
-	}{
-		Name:     opts.Name,
-		Region:   opts.Region,
-		FlavorID: opts.Size,
-		ImageID:  opts.ImageID,
-		SSHKeyID: opts.SSHKeyIDs,
-		UserData: opts.UserData,
-	}
-
-	var result struct {
-		ID      string `json:"id"`
-		Status  string `json:"status"`
-		Created string `json:"created"`
-	}
-
-	err := p.client.Post(path, request, &result)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create instance: %w", err)
-	}
-
-	// Fetch the full instance details
-	createdInstance, err := p.GetInstance(ctx, result.ID)
-	if err != nil {
-		return nil, fmt.Errorf("instance created but failed to retrieve details: %w", err)
-	}
-
-	return createdInstance, nil
-}
-
-// DeleteInstance deletes an instance in OVH
-func (p *OVHProvider) DeleteInstance(ctx context.Context, id string) error {
-	if !p.configured {
-		return errors.New("provider not configured")
-	}
-
-	path := fmt.Sprintf("/cloud/project/%s/instance/%s", p.projectID, id)
-	err := p.client.Delete(path, nil)
-	if err != nil {
-		return fmt.Errorf("failed to delete instance: %w", err)
-	}
-
-	return nil
+// ResetInstance resets an instance in OVH
+func (p *OVHProvider) ResetInstance(ctx context.Context, id string) error {
+	// TODO: Implement this method
+	return errors.New("not implemented")
 }
 
 // StartInstance starts an instance in OVH
 func (p *OVHProvider) StartInstance(ctx context.Context, id string) error {
-	if !p.configured {
-		return errors.New("provider not configured")
-	}
-
-	path := fmt.Sprintf("/cloud/project/%s/instance/%s/start", p.projectID, id)
-	err := p.client.Post(path, nil, nil)
-	if err != nil {
-		return fmt.Errorf("failed to start instance: %w", err)
-	}
-
-	return nil
+	// TODO: Implement this method
+	return errors.New("not implemented")
 }
 
 // StopInstance stops an instance in OVH
 func (p *OVHProvider) StopInstance(ctx context.Context, id string) error {
-	if !p.configured {
-		return errors.New("provider not configured")
-	}
-
-	path := fmt.Sprintf("/cloud/project/%s/instance/%s/stop", p.projectID, id)
-	err := p.client.Post(path, nil, nil)
-	if err != nil {
-		return fmt.Errorf("failed to stop instance: %w", err)
-	}
-
-	return nil
+	// TODO: Implement this method
+	return errors.New("not implemented")
 }
 
 // RestartInstance restarts an instance in OVH
 func (p *OVHProvider) RestartInstance(ctx context.Context, id string) error {
-	if !p.configured {
-		return errors.New("provider not configured")
-	}
-
-	path := fmt.Sprintf("/cloud/project/%s/instance/%s/reboot", p.projectID, id)
-	err := p.client.Post(path, nil, nil)
-	if err != nil {
-		return fmt.Errorf("failed to restart instance: %w", err)
-	}
-
-	return nil
-}
-
-// ListImages lists available OS images in OVH
-func (p *OVHProvider) ListImages(ctx context.Context) ([]Image, error) {
-	if !p.configured {
-		return nil, errors.New("provider not configured")
-	}
-
-	// Get all images
-	path := fmt.Sprintf("/cloud/project/%s/image", p.projectID)
-	var ovhImages []struct {
-		ID           string    `json:"id"`
-		Name         string    `json:"name"`
-		Region       string    `json:"region"`
-		Visibility   string    `json:"visibility"`
-		Type         string    `json:"type"`
-		Status       string    `json:"status"`
-		CreationDate time.Time `json:"creationDate"`
-		MinDisk      int       `json:"minDisk"`
-		Size         int       `json:"size"`
-	}
-
-	err := p.client.Get(path, &ovhImages)
-	if err != nil {
-		return nil, fmt.Errorf("failed to list images: %w", err)
-	}
-
-	images := make([]Image, 0, len(ovhImages))
-	for _, ovhImage := range ovhImages {
-		images = append(images, Image{
-			ID:          ovhImage.ID,
-			Name:        ovhImage.Name,
-			Description: ovhImage.Type,
-			Type:        ovhImage.Type,
-			Status:      ovhImage.Status,
-			CreatedAt:   ovhImage.CreationDate,
-			MinDiskGB:   ovhImage.MinDisk,
-			SizeGB:      ovhImage.Size / (1024 * 1024 * 1024), // Convert bytes to GB
-		})
-	}
-
-	return images, nil
-}
-
-// ListSSHKeys lists SSH keys in OVH
-func (p *OVHProvider) ListSSHKeys(ctx context.Context) ([]SSHKey, error) {
-	if !p.configured {
-		return nil, errors.New("provider not configured")
-	}
-
-	path := fmt.Sprintf("/cloud/project/%s/sshkey", p.projectID)
-	var ovhKeys []struct {
-		ID          string    `json:"id"`
-		Name        string    `json:"name"`
-		PublicKey   string    `json:"publicKey"`
-		Fingerprint string    `json:"fingerprint"`
-		CreatedAt   time.Time `json:"creationDate"`
-	}
-
-	err := p.client.Get(path, &ovhKeys)
-	if err != nil {
-		return nil, fmt.Errorf("failed to list SSH keys: %w", err)
-	}
-
-	keys := make([]SSHKey, 0, len(ovhKeys))
-	for _, ovhKey := range ovhKeys {
-		keys = append(keys, SSHKey{
-			ID:          ovhKey.ID,
-			Name:        ovhKey.Name,
-			PublicKey:   ovhKey.PublicKey,
-			Fingerprint: ovhKey.Fingerprint,
-			CreatedAt:   ovhKey.CreatedAt,
-		})
-	}
-
-	return keys, nil
+	// TODO: Implement this method
+	return errors.New("not implemented")
 }
 
-// CreateSSHKey creates a new SSH key in OVH
-func (p *OVHProvider) CreateSSHKey(ctx context.Context, name, publicKey string) (*SSHKey, error) {
-	if !p.configured {
-		return nil, errors.New("provider not configured")
-	}
-
-	path := fmt.Sprintf("/cloud/project/%s/sshkey", p.projectID)
-	request := struct {
-		Name      string `json:"name"`
-		PublicKey string `json:"publicKey"`
-		Region    string `json:"region,omitempty"`
-	}{
-		Name:      name,
-		PublicKey: publicKey,
-		Region:    p.region, // Optional region
-	}
-
-	var result struct {
-		ID          string    `json:"id"`
-		Name        string    `json:"name"`
-		PublicKey   string    `json:"publicKey"`
-		Fingerprint string    `json:"fingerprint"`
-		CreatedAt   time.Time `json:"creationDate"`
-	}
-
-	err := p.client.Post(path, request, &result)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create SSH key: %w", err)
-	}
-
-	return &SSHKey{
-		ID:          result.ID,
-		Name:        result.Name,
-		PublicKey:   result.PublicKey,
-		Fingerprint: result.Fingerprint,
-		CreatedAt:   result.CreatedAt,
-	}, nil
-}
-
-// DeleteSSHKey deletes an SSH key in OVH
-func (p *OVHProvider) DeleteSSHKey(ctx context.Context, id string) error {
-	if !p.configured {
-		return errors.New("provider not configured")
-	}
-
-	path := fmt.Sprintf("/cloud/project/%s/sshkey/%s", p.projectID, id)
-	err := p.client.Delete(path, nil)
-	if err != nil {
-		return fmt.Errorf("failed to delete SSH key: %w", err)
-	}
-
-	return nil
+// WaitForInstanceStatus waits for an instance to reach a specific status
+func (p *OVHProvider) WaitForInstanceStatus(ctx context.Context, id, status string, timeout time.Duration) error {
+	// TODO: Implement this method
+	return errors.New("not implemented")
 }
 
-// GetInstanceStatus gets the current status of an instance in OVH
-func (p *OVHProvider) GetInstanceStatus(ctx context.Context, id string) (string, error) {
-	instance, err := p.GetInstance(ctx, id)
+// GetFirstFreeInstance retrieves the first available instance
+func (p *OVHProvider) GetFirstFreeInstance(ctx context.Context) (*Instance, error) {
+	// List all instances
+	instances, err := p.ListInstances(ctx)
 	if err != nil {
-		return "", err
+		return nil, fmt.Errorf("failed to list instances: %w", err)
 	}
 
-	return instance.Status, nil
-}
-
-// WaitForInstanceStatus waits for an instance to reach a specific status
-func (p *OVHProvider) WaitForInstanceStatus(ctx context.Context, id, status string, timeout time.Duration) error {
-	deadline := time.Now().Add(timeout)
-
-	for time.Now().Before(deadline) {
-		currentStatus, err := p.GetInstanceStatus(ctx, id)
-		if err != nil {
-			return err
-		}
+	// Iterate through instances to find the first free one
+	for _, instance := range instances {
+		// This will be final optioons using tags
+		// Check if instance has tags and if the instance is marked as available
+		// if instance.DisplayName != nil && instance.Tags["byop-state"] == "available" {
+		// 	// Get full details of the instance
+		// 	vpsPath := fmt.Sprintf("/vps/%s", instance.ID)
+		// 	vps := &models.OVHVPS{}
+		// 	err := p.client.Get(vpsPath, vps)
+		// 	if err != nil {
+		// 		// Log the error but continue with next instance
+		// 		fmt.Printf("Error fetching details for VPS %s: %v\n", instance.ID, err)
+		// 		continue
+		// 	}
+
+		// 	return vps, nil
+		// }
+
+		// Check if display name contains byom.fr or byop.fr
+		if instance.Name != "" && (strings.Contains(instance.Name, "byom.fr") || strings.Contains(instance.Name, "byop.fr")) {
+			// Get full details of the instance
+			vpsPath := fmt.Sprintf("/vps/%s", instance.ID)
+			vps := &models.OVHVPS{}
+			err := p.client.Get(vpsPath, vps)
+			if err != nil {
+				// Log the error but continue with next instance
+				fmt.Printf("Error fetching details for VPS %s: %v\n", instance.ID, err)
+				continue
+			}
 
-		if currentStatus == status {
-			return nil
-		}
+			// Mark this instance as in use
+			if instance.Tags != nil {
+				instance.Tags["byop-state"] = "in-use"
+			}
 
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		case <-time.After(5 * time.Second):
-			// Wait 5 seconds before next check
+			return &Instance{
+				ID:        vps.Name,
+				Name:      vps.DisplayName,
+				IPAddress: vps.Name,
+				Region:    vps.Zone,
+				Size:      strconv.Itoa(vps.VCore),
+				Status:    vps.State,
+			}, nil
 		}
 	}
 
-	return fmt.Errorf("timeout waiting for instance %s to reach status %s", id, status)
-}
-
-// mapOVHStatus maps OVH instance status to standardized status
-func mapOVHStatus(ovhStatus string) string {
-	switch ovhStatus {
-	case "ACTIVE":
-		return "Running"
-	case "BUILD":
-		return "Creating"
-	case "BUILDING":
-		return "Creating"
-	case "SHUTOFF":
-		return "Stopped"
-	case "DELETED":
-		return "Terminated"
-	case "SOFT_DELETED":
-		return "Terminated"
-	case "HARD_REBOOT":
-		return "Restarting"
-	case "REBOOT":
-		return "Restarting"
-	case "RESCUE":
-		return "Running"
-	case "ERROR":
-		return "Error"
-	case "PAUSED":
-		return "Stopped"
-	case "SUSPENDED":
-		return "Stopped"
-	case "STOPPING":
-		return "Stopping"
-	default:
-		return ovhStatus
-	}
+	// If no instance with "available" tag is found, just return the first running instance
+	// if len(instances) > 0 {
+	// 	for _, instance := range instances {
+	// 		if instance.Status == "Running" {
+	// 			vpsPath := fmt.Sprintf("/vps/%s", instance.ID)
+	// 			vps := &models.OVHVPS{}
+	// 			err := p.client.Get(vpsPath, vps)
+	// 			if err != nil {
+	// 				// Log the error but continue
+	// 				fmt.Printf("Error fetching details for VPS %s: %v\n", instance.ID, err)
+	// 				continue
+	// 			}
+
+	// 			// Mark this instance as in use
+	// 			if instance.Tags != nil {
+	// 				instance.Tags["byop-state"] = "in-use"
+	// 			}
+
+	// 			return vps, nil
+	// 		}
+	// 	}
+	// }
+
+	// If no instances are found or none are available, return an error
+	return nil, fmt.Errorf("no free instances found in OVH infrastructure")
 }

+ 68 - 41
cloud/provider.go

@@ -2,7 +2,11 @@ package cloud
 
 import (
 	"context"
+	"fmt"
+	"sync"
 	"time"
+
+	"git.linuxforward.com/byop/byop-engine/models"
 )
 
 // InstanceSize represents the size configuration for a VM instance
@@ -39,14 +43,15 @@ type Instance struct {
 
 // InstanceCreateOpts are options to configure a new instance
 type InstanceCreateOpts struct {
-	Name           string            `json:"name"`
-	Region         string            `json:"region"`
-	Size           string            `json:"size"`
-	ImageID        string            `json:"image_id"`
-	SSHKeyIDs      []string          `json:"ssh_key_ids,omitempty"`
-	UserData       string            `json:"user_data,omitempty"`
-	Tags           map[string]string `json:"tags,omitempty"`
-	SecurityGroups []string          `json:"security_groups,omitempty"`
+	Name           string             `json:"name"`
+	Region         string             `json:"region"`
+	Size           string             `json:"size"`
+	ImageID        string             `json:"image_id"`
+	SSHKeyIDs      []string           `json:"ssh_key_ids,omitempty"`
+	UserData       string             `json:"user_data,omitempty"`
+	Tags           map[string]string  `json:"tags,omitempty"`
+	SecurityGroups []string           `json:"security_groups,omitempty"`
+	Components     []models.Component `json:"components,omitempty"`
 }
 
 // SSHKey represents an SSH key
@@ -75,9 +80,6 @@ type Provider interface {
 	// Initialize sets up the provider with credentials and configuration
 	Initialize(config map[string]string) error
 
-	// Validate checks if the provider credentials are valid
-	Validate(ctx context.Context) (bool, error)
-
 	// ListRegions lists all available regions
 	ListRegions(ctx context.Context) ([]Region, error)
 
@@ -88,13 +90,10 @@ type Provider interface {
 	ListInstances(ctx context.Context) ([]Instance, error)
 
 	// GetInstance gets a specific instance by ID
-	GetInstance(ctx context.Context, id string) (*Instance, error)
-
-	// CreateInstance creates a new instance
-	CreateInstance(ctx context.Context, opts InstanceCreateOpts) (*Instance, error)
+	GetFirstFreeInstance(ctx context.Context) (*Instance, error)
 
 	// DeleteInstance deletes an instance
-	DeleteInstance(ctx context.Context, id string) error
+	ResetInstance(ctx context.Context, id string) error
 
 	// StartInstance starts an instance
 	StartInstance(ctx context.Context, id string) error
@@ -105,49 +104,77 @@ type Provider interface {
 	// RestartInstance restarts an instance
 	RestartInstance(ctx context.Context, id string) error
 
-	// ListImages lists available OS images
-	ListImages(ctx context.Context) ([]Image, error)
-
-	// ListSSHKeys lists SSH keys
-	ListSSHKeys(ctx context.Context) ([]SSHKey, error)
-
-	// CreateSSHKey creates a new SSH key
-	CreateSSHKey(ctx context.Context, name, publicKey string) (*SSHKey, error)
-
-	// DeleteSSHKey deletes an SSH key
-	DeleteSSHKey(ctx context.Context, id string) error
-
-	// GetInstanceStatus gets the current status of an instance
-	GetInstanceStatus(ctx context.Context, id string) (string, error)
-
 	// WaitForInstanceStatus waits for an instance to reach a specific status
 	WaitForInstanceStatus(ctx context.Context, id, status string, timeout time.Duration) error
 }
 
-// ProviderFactory is a function that creates a new provider instance
+// // ProviderFactory is a function that creates a new provider instance
 type ProviderFactory func() Provider
 
-// providers holds a map of provider factories
-var providers = make(map[string]ProviderFactory)
+// providerRegistry manages provider factories and initialized instances
+type providerRegistry struct {
+	factories map[string]ProviderFactory
+	instances map[string]Provider
+	mu        sync.RWMutex
+}
+
+// global registry instance
+var registry = &providerRegistry{
+	factories: make(map[string]ProviderFactory),
+	instances: make(map[string]Provider),
+}
 
 // RegisterProvider registers a new provider factory
 func RegisterProvider(name string, factory ProviderFactory) {
-	providers[name] = factory
+	registry.mu.Lock()
+	defer registry.mu.Unlock()
+	registry.factories[name] = factory
 }
 
-// GetProvider returns a provider by name
-func GetProvider(name string) (Provider, bool) {
-	factory, ok := providers[name]
+// InitializeProvider initializes a provider with config and stores the instance
+func InitializeProvider(name string, config map[string]string) error {
+	registry.mu.Lock()
+	defer registry.mu.Unlock()
+
+	factory, ok := registry.factories[name]
 	if !ok {
-		return nil, false
+		return fmt.Errorf("provider %s not found", name)
+	}
+
+	provider := factory()
+	err := provider.Initialize(config)
+	if err != nil {
+		return err
+	}
+
+	// Store the initialized provider instance
+	registry.instances[name] = provider
+	return nil
+}
+
+// GetProvider returns an initialized provider by name
+func GetProvider(name string) (Provider, bool) {
+	registry.mu.RLock()
+	defer registry.mu.RUnlock()
+
+	// Return the initialized instance if it exists
+	if provider, ok := registry.instances[name]; ok {
+		return provider, true
 	}
-	return factory(), true
+
+	// If there's no initialized instance but there's a factory,
+	// return false to indicate it needs initialization
+	_, ok := registry.factories[name]
+	return nil, ok
 }
 
 // GetSupportedProviders returns a list of supported provider names
 func GetSupportedProviders() []string {
+	registry.mu.RLock()
+	defer registry.mu.RUnlock()
+
 	var names []string
-	for name := range providers {
+	for name := range registry.factories {
 		names = append(names, name)
 	}
 	return names

+ 32 - 17
config.sample.yml

@@ -1,6 +1,7 @@
 # Server configuration
 server:
   host: "0.0.0.0"  # Listen on all interfaces
+<<<<<<< HEAD
   # port: 443       # HTTP port to listen on
   tls:             # TLS/HTTPS configuration
     enabled: false  # Set to true to enable HTTPS
@@ -15,32 +16,46 @@ database:
   password: "secure_password"
   name: "byop_db"
   ssl_mode: "disable"  # Options: disable, require, verify-ca, verify-full
+=======
+  port: 8080       # HTTP port to listen on
+  tls:             
+    enabled: false  # TLS will be handled by Traefik
+
+# Database configuration - Using SQLite
+database:
+  type: "sqlite"  # Database type
+  sqlite:
+    file: "/app/data/byop.db"  # Path inside the container
+>>>>>>> 6009e0299fd96f2b732924eb9e86ebb19b132c8c
 
 # Authentication configuration
 auth:
-  private_key: "your-jwt-signing-key-here"  # Used to sign JWT tokens
+  private_key: "${JWT_SECRET:-change_this_to_a_secure_random_string}"  # Used to sign JWT tokens
   token_duration: 3600000000000  # Token validity duration in nanoseconds (1 hour)
   cleanup_interval: 86400000000000  # Token cleanup interval in nanoseconds (24 hours)
 
 # Cloud providers configuration
+# These values should be set through environment variables in production
 providers:
   # OVH configuration
   ovh:
-    application_key: "your-ovh-app-key"
-    application_secret: "your-ovh-app-secret"
-    consumer_key: "your-ovh-consumer-key"
-    project_id: "your-ovh-project-id"
-    region: "GRA7"  # Optional default region
+    application_key: "${OVH_APP_KEY:-}"
+    application_secret: "${OVH_APP_SECRET:-}"
+    consumer_key: "${OVH_CONSUMER_KEY:-}"
+    project_id: "${OVH_PROJECT_ID:-}"
+    region: "${OVH_REGION:-GRA7}"
+    ssh_username: "${OVH_SSH_USER:-root}"
+    ssh_key_path: "${OVH_SSH_KEY_PATH:-}"
 
-  # AWS configuration
-  aws:
-    access_key: "your-aws-access-key"
-    secret_key: "your-aws-secret-key"
-    region: "us-east-1"
+  # # AWS configuration
+  # aws:
+  #   access_key: "${AWS_ACCESS_KEY:-}"
+  #   secret_key: "${AWS_SECRET_KEY:-}"
+  #   region: "${AWS_REGION:-us-east-1}"
 
-  # Azure configuration
-  azure:
-    subscription_id: "your-azure-subscription-id"
-    tenant_id: "your-azure-tenant-id"
-    client_id: "your-azure-client-id"
-    client_secret: "your-azure-client-secret"
+  # # Azure configuration
+  # azure:
+  #   subscription_id: "${AZURE_SUBSCRIPTION_ID:-}"
+  #   tenant_id: "${AZURE_TENANT_ID:-}"
+  #   client_id: "${AZURE_CLIENT_ID:-}"
+  #   client_secret: "${AZURE_CLIENT_SECRET:-}"

+ 102 - 0
dbstore/blueprint.go

@@ -0,0 +1,102 @@
+package dbstore
+
+import (
+	"fmt"
+
+	"git.linuxforward.com/byop/byop-engine/dbmanager"
+	"git.linuxforward.com/byop/byop-engine/models"
+	"github.com/google/uuid"
+	"gorm.io/gorm"
+)
+
+// BlueprintStore handles database operations for deployment blueprints
+type BlueprintStore struct {
+	db *gorm.DB
+}
+
+// NewBlueprintStore creates a new BlueprintStore
+func NewBlueprintStore(dbManager dbmanager.DbManager) *BlueprintStore {
+	return &BlueprintStore{
+		db: dbManager.GetDB(),
+	}
+}
+
+// Create creates a new blueprint
+func (bs *BlueprintStore) Create(blueprint *models.Blueprint) error {
+	// Generate ID if not provided
+	if blueprint.ID == "" {
+		blueprint.ID = uuid.New().String()
+	}
+
+	// GORM will handle created_at and updated_at automatically
+	return bs.db.Create(blueprint).Error
+}
+
+// GetByID retrieves a blueprint by ID
+func (bs *BlueprintStore) GetByID(id string) (*models.Blueprint, error) {
+	var blueprint models.Blueprint
+	result := bs.db.First(&blueprint, "id = ?", id)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, nil // No blueprint found
+		}
+		return nil, fmt.Errorf("failed to get blueprint: %w", result.Error)
+	}
+	return &blueprint, nil
+}
+
+// Update updates an existing blueprint
+func (bs *BlueprintStore) Update(blueprint *models.Blueprint) error {
+	return bs.db.Save(blueprint).Error
+}
+
+// Delete deletes a blueprint by ID
+func (bs *BlueprintStore) Delete(id string) error {
+	return bs.db.Delete(&models.Blueprint{}, "id = ?", id).Error
+}
+
+// List retrieves all blueprints with optional filtering
+func (bs *BlueprintStore) List(filter map[string]interface{}) ([]*models.Blueprint, error) {
+	var blueprints []*models.Blueprint
+
+	// Build query from filters
+	query := bs.db
+	if filter != nil {
+		for key, value := range filter {
+			query = query.Where(key+" = ?", value)
+		}
+	}
+
+	// Execute query
+	if err := query.Find(&blueprints).Error; err != nil {
+		return nil, fmt.Errorf("failed to list blueprints: %w", err)
+	}
+
+	return blueprints, nil
+}
+
+// GetBlueprintWithDeployments retrieves a blueprint by ID with associated deployments
+func (bs *BlueprintStore) GetBlueprintWithDeployments(id string) (*models.Blueprint, error) {
+	var blueprint models.Blueprint
+	result := bs.db.Preload("Deployments").First(&blueprint, "id = ?", id)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, nil // No blueprint found
+		}
+		return nil, fmt.Errorf("failed to get blueprint: %w", result.Error)
+	}
+	return &blueprint, nil
+}
+
+// GetByVersion retrieves a template by name and version
+func (ts *BlueprintStore) GetByVersion(name string, version string) (*models.Blueprint, error) {
+	var template models.Blueprint
+	result := ts.db.Where("name = ? AND version = ?", name, version).First(&template)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, nil // No template found
+		}
+		return nil, fmt.Errorf("failed to get template: %w", result.Error)
+	}
+	return &template, nil
+}

+ 24 - 24
dbstore/deployment.go

@@ -36,8 +36,8 @@ func (ds *DeploymentStore) Create(deployment *models.Deployment) error {
 		}
 
 		// Create any deployed apps in the same transaction
-		for i := range deployment.DeployedApps {
-			app := &deployment.DeployedApps[i]
+		for i := range deployment.DeployedComponents {
+			app := &deployment.DeployedComponents[i]
 
 			// Ensure ID is 0 so GORM can auto-generate it
 			app.ID = 0
@@ -90,14 +90,14 @@ func (ds *DeploymentStore) GetByID(id int64) (*models.Deployment, error) {
 	}
 
 	// Load resources for each deployed app
-	for i, app := range deployment.DeployedApps {
-		var resource models.DeployedAppResource
+	for i, app := range deployment.DeployedComponents {
+		var resource models.DeployedComponentResource
 		if err := ds.db.Where("deployed_app_id = ?", app.ID).First(&resource).Error; err != nil {
 			if err != gorm.ErrRecordNotFound {
 				return nil, fmt.Errorf("failed to get resources for deployed app: %w", err)
 			}
 		} else {
-			deployment.DeployedApps[i].Resources = models.ResourceAllocation{
+			deployment.DeployedComponents[i].Resources = models.ResourceAllocation{
 				CPU:          resource.CPU,
 				CPUUsage:     resource.CPUUsage,
 				Memory:       resource.Memory,
@@ -131,8 +131,8 @@ func (ds *DeploymentStore) Update(deployment *models.Deployment) error {
 		}
 
 		// Handle deployed apps - this is trickier as we need to compare with existing apps
-		var existingApps []models.DeployedApp
-		if err := tx.Where("deployment_id = ?", deployment.ID).Find(&existingApps).Error; err != nil {
+		var existingComponents []models.DeployedComponent
+		if err := tx.Where("deployment_id = ?", deployment.ID).Find(&existingComponents).Error; err != nil {
 			return err
 		}
 
@@ -143,15 +143,15 @@ func (ds *DeploymentStore) Update(deployment *models.Deployment) error {
 		}
 
 		// Process each app in the updated deployment
-		for i := range deployment.DeployedApps {
-			app := &deployment.DeployedApps[i]
+		for i := range deployment.DeployedComponents {
+			app := &deployment.DeployedComponents[i]
 
 			// If app has ID and exists, update it
 			if app.ID != 0 && existingAppMap[app.ID] {
 				if err := tx.Save(app).Error; err != nil {
 					return err
 				}
-				delete(existingAppMap, app.ID)
+				delete(existingComponentMap, app.ID)
 			} else {
 				// New app, create it (GORM will auto-generate ID)
 				app.ID = 0 // Ensure ID is 0 for auto-increment
@@ -163,7 +163,7 @@ func (ds *DeploymentStore) Update(deployment *models.Deployment) error {
 
 			// Handle resources
 			if app.Resources != (models.ResourceAllocation{}) {
-				var resource models.DeployedAppResource
+				var resource models.DeployedComponentResource
 				result := tx.Where("deployed_app_id = ?", app.ID).First(&resource)
 				if result.Error != nil && result.Error != gorm.ErrRecordNotFound {
 					return result.Error
@@ -200,12 +200,12 @@ func (ds *DeploymentStore) Update(deployment *models.Deployment) error {
 		}
 
 		// Delete any apps that are no longer part of the deployment
-		for appID := range existingAppMap {
-			if err := tx.Delete(&models.DeployedApp{}, "id = ?", appID).Error; err != nil {
+		for appID := range existingComponentMap {
+			if err := tx.Delete(&models.DeployedComponent{}, "id = ?", appID).Error; err != nil {
 				return err
 			}
 			// Delete associated resources
-			if err := tx.Delete(&models.DeployedAppResource{}, "deployed_app_id = ?", appID).Error; err != nil && err != gorm.ErrRecordNotFound {
+			if err := tx.Delete(&models.DeployedComponentResource{}, "deployed_app_id = ?", appID).Error; err != nil && err != gorm.ErrRecordNotFound {
 				return err
 			}
 		}
@@ -217,20 +217,20 @@ func (ds *DeploymentStore) Update(deployment *models.Deployment) error {
 // Delete deletes a deployment by ID
 func (ds *DeploymentStore) Delete(id int64) error {
 	return ds.db.Transaction(func(tx *gorm.DB) error {
-		// Delete associated DeployedAppResources
-		var deployedApps []models.DeployedApp
-		if err := tx.Where("deployment_id = ?", id).Find(&deployedApps).Error; err != nil {
+		// Delete associated DeployedComponentResources
+		var deployedComponents []models.DeployedComponent
+		if err := tx.Where("deployment_id = ?", id).Find(&deployedComponents).Error; err != nil {
 			return err
 		}
 
-		for _, app := range deployedApps {
-			if err := tx.Delete(&models.DeployedAppResource{}, "deployed_app_id = ?", app.ID).Error; err != nil && err != gorm.ErrRecordNotFound {
+		for _, app := range deployedComponents {
+			if err := tx.Delete(&models.DeployedComponentResource{}, "deployed_app_id = ?", app.ID).Error; err != nil && err != gorm.ErrRecordNotFound {
 				return err
 			}
 		}
 
 		// Delete deployed apps
-		if err := tx.Delete(&models.DeployedApp{}, "deployment_id = ?", id).Error; err != nil && err != gorm.ErrRecordNotFound {
+		if err := tx.Delete(&models.DeployedComponent{}, "deployment_id = ?", id).Error; err != nil && err != gorm.ErrRecordNotFound {
 			return err
 		}
 
@@ -244,7 +244,7 @@ func (ds *DeploymentStore) List(filter map[string]interface{}) ([]*models.Deploy
 	var deployments []*models.Deployment
 
 	// Build query from filters
-	query := ds.db.Preload("DeployedApps")
+	query := ds.db.Preload("DeployedComponents")
 	if filter != nil {
 		for key, value := range filter {
 			query = query.Where(key+" = ?", value)
@@ -259,14 +259,14 @@ func (ds *DeploymentStore) List(filter map[string]interface{}) ([]*models.Deploy
 	// Load resources and deserialize config for each deployment
 	for i, deployment := range deployments {
 		// Load resources for each deployed app
-		for j, app := range deployment.DeployedApps {
-			var resource models.DeployedAppResource
+		for j, app := range deployment.DeployedComponents {
+			var resource models.DeployedComponentResource
 			if err := ds.db.Where("deployed_app_id = ?", app.ID).First(&resource).Error; err != nil {
 				if err != gorm.ErrRecordNotFound {
 					return nil, fmt.Errorf("failed to get resources for deployed app: %w", err)
 				}
 			} else {
-				deployments[i].DeployedApps[j].Resources = models.ResourceAllocation{
+				deployments[i].DeployedComponents[j].Resources = models.ResourceAllocation{
 					CPU:          resource.CPU,
 					CPUUsage:     resource.CPUUsage,
 					Memory:       resource.Memory,

+ 327 - 0
docs/git_deployment.md

@@ -0,0 +1,327 @@
+# Git-based Deployment in BYOP Engine
+
+## Overview
+
+The BYOP Engine now supports Git-based deployments using Git hooks for continuous deployment. This allows developers to deploy applications by simply pushing to a Git repository.
+
+## How It Works
+
+1. **Initial Setup**: When a VM is initialized:
+   - Creates a bare Git repository on the VM
+   - Sets up a working directory for the component
+   - Configures Git hooks for automatic deployment
+
+2. **Continuous Deployment**: After initial setup, developers can:
+   - Add the remote repository to their local Git config
+   - Push changes to trigger automatic deployment
+   - Monitor deployment progress through the BYOP dashboard
+
+3. **Component-Specific Deployment**: Different components are handled appropriately:
+   - **Frontend**: Built and served via Nginx
+   - **Backend**: Built and managed via systemd or PM2
+   - **Database**: Configuration files applied and services restarted
+
+## Usage
+
+### Adding a Remote Repository
+
+After a component is deployed, add the remote repository to your Git config:
+
+```bash
+git remote add production ssh://root@<vm-ip>/opt/byop/repos/<component-id>.git
+```
+
+### Deploying Changes
+
+Push to the remote repository to trigger a deployment:
+
+```bash
+git push production <branch>
+```
+
+The post-receive hook will automatically:
+1. Check out the code to the working directory
+2. Install dependencies
+3. Build the application
+4. Restart or reload services as needed
+
+### Monitoring Deployments
+
+You can monitor deployment status through:
+- The BYOP dashboard
+- SSH access to the VM to check logs
+- Component status indicators
+
+## Security Considerations
+
+- SSH access is controlled through credentials managed by BYOP
+- Deploy keys can be configured for secure repository access
+- All operations use secure SSH connections
+
+## Future Enhancements
+
+- Support for deployment rollbacks
+- Automated testing before deployment
+- Multi-stage deployment environments (dev, staging, production)
+- Notification system for deployment status updates
+
+## Post script hooks example
+
+```golang
+
+// createFrontendPostReceiveHook generates a Git hook for frontend components
+func createFrontendPostReceiveHook(component models.Component, deployPath string) string {
+	return fmt.Sprintf(`#!/bin/bash
+echo "Deploying frontend component: %s"
+
+# Get the target branch (usually main or master)
+TARGET="%s"
+while read oldrev newrev ref
+do
+    # Check if the pushed branch is our target branch
+    if [[ $ref = refs/heads/$TARGET ]]; 
+    then
+        echo "Deploying $TARGET branch..."
+        
+        # Checkout code to the deployment directory
+        GIT_WORK_TREE=%s git checkout -f $TARGET
+        cd %s
+        
+        # Update environment variables
+        echo '%s' > %s/.env
+        
+        # Install dependencies
+        echo "Installing dependencies..."
+        npm install
+        
+        # Build the application
+        echo "Building application..."
+        %s
+        
+        # Notify about completion
+        echo "Frontend deployment completed successfully"
+    fi
+done
+`, component.Name, component.Branch, deployPath, deployPath, component.EnvVariables, deployPath, component.BuildCommand)
+}
+
+// createGoPostReceiveHook generates a Git hook for Go components
+func createGoPostReceiveHook(component models.Component, deployPath string) string {
+	return fmt.Sprintf(`#!/bin/bash
+echo "Deploying Go component: %s"
+
+# Get the target branch (usually main or master)
+TARGET="%s"
+while read oldrev newrev ref
+do
+    # Check if the pushed branch is our target branch
+    if [[ $ref = refs/heads/$TARGET ]]; 
+    then
+        echo "Deploying $TARGET branch..."
+        
+        # Checkout code to the deployment directory
+        GIT_WORK_TREE=%s git checkout -f $TARGET
+        cd %s
+        
+        # Update environment variables
+        echo '%s' > %s/.env
+        
+        # Build the application
+        echo "Building Go application..."
+        go build -o app
+        
+        # Restart the service
+        echo "Restarting service..."
+        systemctl restart byop-%s
+        
+        # Notify about completion
+        echo "Go deployment completed successfully"
+    fi
+done
+`, component.Name, component.Branch, deployPath, deployPath, component.EnvVariables, deployPath, component.ID)
+}
+
+// createNodePostReceiveHook generates a Git hook for Node.js components
+func createNodePostReceiveHook(component models.Component, deployPath string) string {
+	return fmt.Sprintf(`#!/bin/bash
+echo "Deploying Node.js component: %s"
+
+# Get the target branch (usually main or master)
+TARGET="%s"
+while read oldrev newrev ref
+do
+    # Check if the pushed branch is our target branch
+    if [[ $ref = refs/heads/$TARGET ]]; 
+    then
+        echo "Deploying $TARGET branch..."
+        
+        # Checkout code to the deployment directory
+        GIT_WORK_TREE=%s git checkout -f $TARGET
+        cd %s
+        
+        # Update environment variables
+        echo '%s' > %s/.env
+        
+        # Install dependencies
+        echo "Installing dependencies..."
+        npm install
+        
+        # Build the application if there's a build command
+        if [[ "%s" != "" ]]; then
+            echo "Building application..."
+            %s || true
+        fi
+        
+        # Restart the PM2 process
+        echo "Restarting PM2 process..."
+        pm2 restart byop-%s || pm2 start npm --name "byop-%s" -- start
+        pm2 save
+        
+        # Notify about completion
+        echo "Node.js deployment completed successfully"
+    fi
+done
+`, component.Name, component.Branch, deployPath, deployPath, component.EnvVariables, deployPath, component.BuildCommand, component.BuildCommand, component.ID, component.ID)
+}
+
+// createPythonPostReceiveHook generates a Git hook for Python components
+func createPythonPostReceiveHook(component models.Component, deployPath string) string {
+	return fmt.Sprintf(`#!/bin/bash
+echo "Deploying Python component: %s"
+
+# Get the target branch (usually main or master)
+TARGET="%s"
+while read oldrev newrev ref
+do
+    # Check if the pushed branch is our target branch
+    if [[ $ref = refs/heads/$TARGET ]]; 
+    then
+        echo "Deploying $TARGET branch..."
+        
+        # Checkout code to the deployment directory
+        GIT_WORK_TREE=%s git checkout -f $TARGET
+        cd %s
+        
+        # Update environment variables
+        echo '%s' > %s/.env
+        
+        # Update dependencies
+        echo "Updating Python dependencies..."
+        source venv/bin/activate
+        pip install -r requirements.txt
+        
+        # Restart the service
+        echo "Restarting service..."
+        systemctl restart byop-%s
+        
+        # Notify about completion
+        echo "Python deployment completed successfully"
+    fi
+done
+`, component.Name, component.Branch, deployPath, deployPath, component.EnvVariables, deployPath, component.ID)
+}
+
+// createDatabasePostReceiveHook generates a Git hook for database components
+func createDatabasePostReceiveHook(component models.Component, deployPath string, dbType string) string {
+	var configUpdate, restartCmd string
+
+	switch dbType {
+	case "postgresql":
+		configUpdate = fmt.Sprintf(`
+        # Apply configuration changes if available
+        if [ -f %s/postgresql.conf ]; then
+            cp %s/postgresql.conf /etc/postgresql/*/main/
+            echo "Updated PostgreSQL configuration"
+        fi`, deployPath, deployPath)
+		restartCmd = "systemctl restart postgresql"
+	case "mariadb", "mysql":
+		configUpdate = fmt.Sprintf(`
+        # Apply configuration changes if available
+        if [ -f %s/my.cnf ]; then
+            cp %s/my.cnf /etc/mysql/
+            echo "Updated MariaDB configuration"
+        fi`, deployPath, deployPath)
+		restartCmd = "systemctl restart mariadb"
+	case "mongodb":
+		configUpdate = fmt.Sprintf(`
+        # Apply configuration changes if available
+        if [ -f %s/mongodb.conf ]; then
+            cp %s/mongodb.conf /etc/mongodb.conf
+            echo "Updated MongoDB configuration"
+        fi`, deployPath, deployPath)
+		restartCmd = "systemctl restart mongodb"
+	}
+
+	return fmt.Sprintf(`#!/bin/bash
+echo "Deploying database component: %s"
+
+# Get the target branch (usually main or master)
+TARGET="%s"
+while read oldrev newrev ref
+do
+    # Check if the pushed branch is our target branch
+    if [[ $ref = refs/heads/$TARGET ]]; 
+    then
+        echo "Deploying $TARGET branch..."
+        
+        # Checkout code to the deployment directory
+        GIT_WORK_TREE=%s git checkout -f $TARGET
+        cd %s
+        
+        # Update environment variables
+        echo '%s' > %s/.env
+        %s
+        
+        # Run any database migrations if available
+        if [ -f %s/migrations/run.sh ]; then
+            echo "Running database migrations..."
+            bash %s/migrations/run.sh
+        fi
+        
+        # Restart database service
+        echo "Restarting database service..."
+        %s
+        
+        # Notify about completion
+        echo "Database component deployment completed successfully"
+    fi
+done
+`, component.Name, component.Branch, deployPath, deployPath, component.EnvVariables, deployPath, configUpdate, deployPath, deployPath, restartCmd)
+}
+
+// createSystemdServiceCommand creates a systemd service file for the component
+func createSystemdServiceCommand(component models.Component, deploymentPath string) string {
+	var execStart string
+	var workingDir string
+
+	workingDir = deploymentPath
+
+	switch component.Language {
+	case "golang":
+		execStart = fmt.Sprintf("%s/app", deploymentPath)
+	case "python":
+		execStart = fmt.Sprintf("%s/venv/bin/python %s/main.py", deploymentPath, deploymentPath)
+	default:
+		execStart = component.BuildCommand
+	}
+
+	serviceFile := fmt.Sprintf(`[Unit]
+Description=BYOP Component %s
+After=network.target
+
+[Service]
+ExecStart=%s
+WorkingDirectory=%s
+Restart=always
+User=root
+Group=root
+Environment=PATH=/usr/bin:/usr/local/bin
+EnvironmentFile=%s/.env
+
+[Install]
+WantedBy=multi-user.target
+`, component.Name, execStart, workingDir, deploymentPath)
+
+	return fmt.Sprintf("echo '%s' > /etc/systemd/system/byop-%s.service", serviceFile, component.ID)
+}
+```

+ 152 - 0
handlers/blueprints.go

@@ -0,0 +1,152 @@
+package handlers
+
+import (
+	"fmt"
+	"net/http"
+
+	"git.linuxforward.com/byop/byop-engine/models"
+	"git.linuxforward.com/byop/byop-engine/services"
+	"github.com/gin-gonic/gin"
+)
+
+// BlueprintHandler handles blueprint-related operations
+type BlueprintHandler struct {
+	service *services.BlueprintService
+}
+
+// NewBlueprintHandler creates a new BlueprintHandler
+func NewBlueprintHandler(service *services.BlueprintService) *BlueprintHandler {
+	return &BlueprintHandler{
+		service: service,
+	}
+}
+
+// ListBlueprints returns all blueprints with optional filtering
+func (h *BlueprintHandler) ListBlueprints(c *gin.Context) {
+	filter := make(map[string]interface{})
+
+	// Attempt to bind query parameters, but allow empty filters
+	if err := c.ShouldBindQuery(&filter); err != nil && len(filter) > 0 {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid query parameters"})
+		return
+	}
+
+	blueprints, err := h.service.ListBlueprints(filter)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to list blueprints: %v", err)})
+		return
+	}
+
+	c.JSON(http.StatusOK, blueprints)
+}
+
+// CreateBlueprint creates a new deployment blueprint
+func (h *BlueprintHandler) CreateBlueprint(c *gin.Context) {
+	var blueprint models.Blueprint
+
+	if err := c.ShouldBindJSON(&blueprint); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request body: %v", err)})
+		return
+	}
+
+	// Get the user ID from the context (set by auth middleware)
+	userID, exists := c.Get("userID")
+	if exists {
+		blueprint.CreatedBy = userID.(string)
+	}
+
+	if err := h.service.CreateBlueprint(&blueprint); err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create blueprint: %v", err)})
+		return
+	}
+
+	c.JSON(http.StatusCreated, blueprint)
+}
+
+// GetBlueprint returns a specific blueprint
+func (h *BlueprintHandler) GetBlueprint(c *gin.Context) {
+	id := c.Param("id")
+
+	blueprint, err := h.service.GetBlueprint(id)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to fetch blueprint: %v", err)})
+		return
+	}
+
+	if blueprint == nil {
+		c.JSON(http.StatusNotFound, gin.H{"error": "Blueprint not found"})
+		return
+	}
+
+	c.JSON(http.StatusOK, blueprint)
+}
+
+// UpdateBlueprint updates a blueprint
+func (h *BlueprintHandler) UpdateBlueprint(c *gin.Context) {
+	id := c.Param("id")
+
+	var updatedBlueprint models.Blueprint
+	if err := c.ShouldBindJSON(&updatedBlueprint); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request body: %v", err)})
+		return
+	}
+
+	// Ensure the ID matches the URL parameter
+	updatedBlueprint.ID = id
+
+	if err := h.service.UpdateBlueprint(&updatedBlueprint); err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update blueprint: %v", err)})
+		return
+	}
+
+	c.JSON(http.StatusOK, updatedBlueprint)
+}
+
+// DeleteBlueprint deletes a blueprint
+func (h *BlueprintHandler) DeleteBlueprint(c *gin.Context) {
+	id := c.Param("id")
+
+	if err := h.service.DeleteBlueprint(id); err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to delete blueprint: %v", err)})
+		return
+	}
+
+	c.Status(http.StatusNoContent)
+}
+
+// GetBlueprintDeployments returns all deployments for a template
+func (h *BlueprintHandler) GetBlueprintDeployments(c *gin.Context) {
+	id := c.Param("id")
+
+	deployments, err := h.service.GetBlueprintDeployments(id)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to fetch template deployments: %v", err)})
+		return
+	}
+
+	c.JSON(http.StatusOK, deployments)
+}
+
+// GetBlueprintByVersion handles retrieval of a template by name and version
+func (h *BlueprintHandler) GetBlueprintByVersion(c *gin.Context) {
+	name := c.Query("name")
+	version := c.Query("version")
+
+	if name == "" || version == "" {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "Both name and version parameters are required"})
+		return
+	}
+
+	template, err := h.service.GetBlueprintByVersion(name, version)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to fetch template: %v", err)})
+		return
+	}
+
+	if template == nil {
+		c.JSON(http.StatusNotFound, gin.H{"error": "Blueprint not found"})
+		return
+	}
+
+	c.JSON(http.StatusOK, template)
+}

+ 1 - 1
handlers/deployments.go

@@ -31,7 +31,7 @@ func (h *DeploymentHandler) RegisterRoutes(r *gin.RouterGroup) {
 	r.DELETE("/:id", h.DeleteDeployment)
 	r.PUT("/:id/status", h.UpdateDeploymentStatus)
 	r.GET("/by-client/:clientId", h.GetDeploymentsByClient)
-	r.GET("/by-template/:templateId", h.GetDeploymentsByTemplate)
+	r.GET("/by-blueprint/:blueprintId", h.GetDeploymentsByBlueprint)
 	r.GET("/by-user/:userId", h.GetDeploymentsByUser)
 }
 

+ 43 - 2
middleware/auth.go

@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"context"
+	"fmt"
 	"net/http"
 
 	"git.linuxforward.com/byop/byop-engine/auth"
@@ -42,15 +43,20 @@ func Auth(authService auth.Service) gin.HandlerFunc {
 		}
 
 		// Validate token using the auth service
-		clientID, err := authService.ValidateToken(c.Request.Context(), token)
+		clientID, role, err := authService.ValidateToken(c.Request.Context(), token)
 		if err != nil {
 			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
 			c.Abort()
 			return
 		}
 
-		// Set client ID in context for later use
+		fmt.Println("Client ID from token:", clientID)
+		fmt.Println("Role from token:", role)
+
+		// Set client ID and role in context for later use
 		c.Set("clientID", clientID)
+		c.Set("user_id", clientID) // Set user_id for backward compatibility
+		c.Set("role", role)
 		c.Next()
 	}
 }
@@ -76,6 +82,7 @@ func GetRoleFromContext(ctx context.Context) string {
 // IsAdmin checks if the user in the context has admin role
 func IsAdmin(ctx context.Context) bool {
 	role := GetRoleFromContext(ctx)
+	fmt.Println("Role from context:", role)
 	return role == "admin"
 }
 
@@ -93,3 +100,37 @@ func extractTokenFromHeader(c *gin.Context) string {
 
 	return ""
 }
+
+// AdminAuth middleware checks if the user has admin role
+func AdminAuth(authService auth.Service) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// Get token from request
+		token := extractTokenFromHeader(c)
+		if token == "" {
+			c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
+			c.Abort()
+			return
+		}
+
+		// Validate token using the auth service
+		clientID, role, err := authService.ValidateToken(c.Request.Context(), token)
+		if err != nil {
+			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
+			c.Abort()
+			return
+		}
+
+		// Check if the user has admin role
+		if role != "admin" {
+			c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"})
+			c.Abort()
+			return
+		}
+
+		// Set client ID and role in context for later use
+		c.Set("clientID", clientID)
+		c.Set("user_id", clientID) // Set user_id for backward compatibility
+		c.Set("role", role)
+		c.Next()
+	}
+}

+ 41 - 0
middleware/cors.go

@@ -0,0 +1,41 @@
+package middleware
+
+import (
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+	"github.com/sirupsen/logrus"
+)
+
+// CORS handles Cross-Origin Resource Sharing (CORS) for the API
+func CORS() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		origin := c.Request.Header.Get("Origin")
+
+		// Debug logging
+		logger := logrus.WithFields(logrus.Fields{
+			"origin": origin,
+			"path":   c.Request.URL.Path,
+			"method": c.Request.Method,
+		})
+		logger.Debug("CORS request received")
+
+		// For development, allow all origins
+		// In production, you would want to restrict this
+		if origin != "" {
+			c.Header("Access-Control-Allow-Origin", origin)
+			c.Header("Access-Control-Allow-Credentials", "true")
+			c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
+			c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Accept, X-Requested-With")
+			c.Header("Access-Control-Expose-Headers", "Content-Length, Content-Type, Authorization")
+		}
+
+		// Handle preflight requests
+		if c.Request.Method == http.MethodOptions {
+			c.AbortWithStatus(http.StatusOK)
+			return
+		}
+
+		c.Next()
+	}
+}

+ 0 - 99
models/apps.go

@@ -1,99 +0,0 @@
-package models
-
-import (
-	"encoding/json"
-	"time"
-
-	"gorm.io/gorm"
-)
-
-type App struct {
-	ID          int64  `gorm:"column:rowid;primaryKey;autoIncrement" json:"id"` // Unique identifier
-	Name        string `json:"name" gorm:"not null"`
-	Description string `json:"description"`
-	Version     string `json:"version" gorm:"index"`
-
-	// Configuration as JSON string in DB
-	ConfigJSON string `json:"-" gorm:"column:config;type:text"`
-
-	// Virtual field for ORM serialization/deserialization
-	Config AppConfig `json:"config" gorm:"-"`
-
-	CreatedAt time.Time      `json:"createdAt" gorm:"autoCreateTime"`
-	UpdatedAt time.Time      `json:"updatedAt" gorm:"autoUpdateTime"`
-	CreatedBy string         `json:"createdBy" gorm:"index"` // User ID who created the template
-	DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`         // Soft delete support
-
-	// Relationships
-	Deployments []Deployment `json:"deployments" gorm:"foreignKey:AppID"` // Deployments using this app
-}
-
-type AppConfig struct {
-	Components      []ComponentConfig `json:"components"`             // Components included in this app (renamed from apps)
-	NetworkPolicies []NetworkPolicy   `json:"networkPolicies"`        // Network policies to apply
-	EnvVariables    map[string]string `json:"envVariables,omitempty"` // Environment variables
-	Secrets         []SecretConfig    `json:"secrets,omitempty"`      // Secret configurations
-}
-
-type ComponentConfig struct {
-	ID           int64             `json:"id"`                     // Reference to the component
-	Name         string            `json:"name"`                   // Name of the component in this app
-	ExposedPorts []int             `json:"exposedPorts,omitempty"` // Ports to expose
-	PublicAccess bool              `json:"publicAccess"`           // Whether the component is publicly accessible
-	Resources    ResourceConfig    `json:"resources"`              // Resource allocation
-	Autoscaling  AutoscalingConfig `json:"autoscaling,omitempty"`  // Autoscaling configuration
-	EnvOverrides map[string]string `json:"envOverrides,omitempty"` // Environment variable overrides
-	ServiceMesh  bool              `json:"serviceMesh"`            // Whether to include in service mesh
-}
-
-type ResourceConfig struct {
-	CPU     string `json:"cpu"`     // e.g., "0.5"
-	Memory  string `json:"memory"`  // e.g., "512Mi"
-	Storage string `json:"storage"` // e.g., "1Gi"
-}
-
-type AutoscalingConfig struct {
-	Enabled      bool   `json:"enabled"`      // Whether autoscaling is enabled
-	MinReplicas  int    `json:"minReplicas"`  // Minimum number of replicas
-	MaxReplicas  int    `json:"maxReplicas"`  // Maximum number of replicas
-	CPUThreshold int    `json:"cpuThreshold"` // CPU threshold for scaling (percentage)
-	Metric       string `json:"metric"`       // Metric to base scaling on (e.g., "cpu", "memory")
-}
-
-type NetworkPolicy struct {
-	Name           string   `json:"name"`            // Policy name
-	FromComponents []string `json:"fromComponents"`  // Source components (renamed from FromApps)
-	ToComponents   []string `json:"toComponents"`    // Destination components (renamed from ToApps)
-	Ports          []int    `json:"ports,omitempty"` // Allowed ports
-	AllowEgress    bool     `json:"allowEgress"`     // Whether to allow egress traffic
-}
-
-type SecretConfig struct {
-	Name        string `json:"name"`        // Secret name
-	Description string `json:"description"` // Secret description
-	Required    bool   `json:"required"`    // Whether the secret is required
-}
-
-// BeforeSave serializes the embedded JSON fields
-func (a *App) BeforeSave(tx *gorm.DB) error {
-	// Marshal Config to JSON
-	configJSON, err := json.Marshal(a.Config)
-	if err != nil {
-		return err
-	}
-	a.ConfigJSON = string(configJSON)
-
-	return nil
-}
-
-// AfterFind deserializes the JSON fields
-func (a *App) AfterFind(tx *gorm.DB) error {
-	// Unmarshal Config from JSON
-	if a.ConfigJSON != "" {
-		if err := json.Unmarshal([]byte(a.ConfigJSON), &a.Config); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}

+ 99 - 0
models/blueprint.go

@@ -0,0 +1,99 @@
+package models
+
+import (
+	"encoding/json"
+	"time"
+
+	"gorm.io/gorm"
+)
+
+type Blueprint struct {
+	ID          string `json:"id" gorm:"primaryKey"`
+	Name        string `json:"name" gorm:"not null"`
+	Description string `json:"description"`
+	Version     string `json:"version" gorm:"index"`
+
+	// Configuration as JSON string in DB
+	ConfigJSON string `json:"-" gorm:"column:config;type:text"`
+
+	// Virtual field for ORM serialization/deserialization
+	Config BlueprintConfig `json:"config" gorm:"-"`
+
+	CreatedAt time.Time      `json:"createdAt" gorm:"autoCreateTime"`
+	UpdatedAt time.Time      `json:"updatedAt" gorm:"autoUpdateTime"`
+	CreatedBy string         `json:"createdBy" gorm:"index"` // User ID who created the blueprint
+	DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`         // Soft delete support
+
+	// Relationships
+	Deployments []Deployment `json:"deployments" gorm:"foreignKey:BlueprintID"` // Deployments using this blueprint
+}
+
+type BlueprintConfig struct {
+	Components      []ComponentConfig `json:"components"`             // Components included in this blueprint
+	NetworkPolicies []NetworkPolicy   `json:"networkPolicies"`        // Network policies to apply
+	EnvVariables    map[string]string `json:"envVariables,omitempty"` // Environment variables
+	Secrets         []SecretConfig    `json:"secrets,omitempty"`      // Secret configurations
+}
+
+type ComponentConfig struct {
+	ID           string            `json:"id"`                     // Reference to the component
+	Name         string            `json:"name"`                   // Name of the component in this blueprint
+	ExposedPorts []int             `json:"exposedPorts,omitempty"` // Ports to expose
+	PublicAccess bool              `json:"publicAccess"`           // Whether the component is publicly accessible
+	Resources    ResourceConfig    `json:"resources"`              // Resource allocation
+	Autoscaling  AutoscalingConfig `json:"autoscaling,omitempty"`  // Autoscaling configuration
+	EnvOverrides map[string]string `json:"envOverrides,omitempty"` // Environment variable overrides
+	ServiceMesh  bool              `json:"serviceMesh"`            // Whether to include in service mesh
+}
+
+type ResourceConfig struct {
+	CPU     string `json:"cpu"`     // e.g., "0.5"
+	Memory  string `json:"memory"`  // e.g., "512Mi"
+	Storage string `json:"storage"` // e.g., "1Gi"
+}
+
+type AutoscalingConfig struct {
+	Enabled      bool   `json:"enabled"`      // Whether autoscaling is enabled
+	MinReplicas  int    `json:"minReplicas"`  // Minimum number of replicas
+	MaxReplicas  int    `json:"maxReplicas"`  // Maximum number of replicas
+	CPUThreshold int    `json:"cpuThreshold"` // CPU threshold for scaling (percentage)
+	Metric       string `json:"metric"`       // Metric to base scaling on (e.g., "cpu", "memory")
+}
+
+type NetworkPolicy struct {
+	Name           string   `json:"name"`            // Policy name
+	FromComponents []string `json:"fromComponents"`  // Source components
+	ToComponents   []string `json:"toComponents"`    // Destination components
+	Ports          []int    `json:"ports,omitempty"` // Allowed ports
+	AllowEgress    bool     `json:"allowEgress"`     // Whether to allow egress traffic
+}
+
+type SecretConfig struct {
+	Name        string `json:"name"`        // Secret name
+	Description string `json:"description"` // Secret description
+	Required    bool   `json:"required"`    // Whether the secret is required
+}
+
+// BeforeSave serializes the embedded JSON fields
+func (b *Blueprint) BeforeSave(tx *gorm.DB) error {
+	// Marshal Config to JSON
+	configJSON, err := json.Marshal(b.Config)
+	if err != nil {
+		return err
+	}
+	b.ConfigJSON = string(configJSON)
+
+	return nil
+}
+
+// AfterFind deserializes the JSON fields
+func (b *Blueprint) AfterFind(tx *gorm.DB) error {
+	// Unmarshal Config from JSON
+	if b.ConfigJSON != "" {
+		if err := json.Unmarshal([]byte(b.ConfigJSON), &b.Config); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}

+ 99 - 0
models/component.go

@@ -0,0 +1,99 @@
+package models
+
+import (
+	"encoding/json"
+	"time"
+
+	"gorm.io/gorm"
+)
+
+type App struct {
+	ID          int64  `gorm:"column:rowid;primaryKey;autoIncrement" json:"id"` // Unique identifier
+	Name        string `json:"name" gorm:"not null"`
+	Description string `json:"description"`
+	Version     string `json:"version" gorm:"index"`
+
+	// Configuration as JSON string in DB
+	ConfigJSON string `json:"-" gorm:"column:config;type:text"`
+
+	// Virtual field for ORM serialization/deserialization
+	Config AppConfig `json:"config" gorm:"-"`
+
+	CreatedAt time.Time      `json:"createdAt" gorm:"autoCreateTime"`
+	UpdatedAt time.Time      `json:"updatedAt" gorm:"autoUpdateTime"`
+	CreatedBy string         `json:"createdBy" gorm:"index"` // User ID who created the template
+	DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`         // Soft delete support
+
+	// Relationships
+	Deployments []Deployment `json:"deployments" gorm:"foreignKey:AppID"` // Deployments using this app
+}
+
+type AppConfig struct {
+	Components      []ComponentConfig `json:"components"`             // Components included in this app (renamed from apps)
+	NetworkPolicies []NetworkPolicy   `json:"networkPolicies"`        // Network policies to apply
+	EnvVariables    map[string]string `json:"envVariables,omitempty"` // Environment variables
+	Secrets         []SecretConfig    `json:"secrets,omitempty"`      // Secret configurations
+}
+
+type ComponentConfig struct {
+	ID           int64             `json:"id"`                     // Reference to the component
+	Name         string            `json:"name"`                   // Name of the component in this app
+	ExposedPorts []int             `json:"exposedPorts,omitempty"` // Ports to expose
+	PublicAccess bool              `json:"publicAccess"`           // Whether the component is publicly accessible
+	Resources    ResourceConfig    `json:"resources"`              // Resource allocation
+	Autoscaling  AutoscalingConfig `json:"autoscaling,omitempty"`  // Autoscaling configuration
+	EnvOverrides map[string]string `json:"envOverrides,omitempty"` // Environment variable overrides
+	ServiceMesh  bool              `json:"serviceMesh"`            // Whether to include in service mesh
+}
+
+type ResourceConfig struct {
+	CPU     string `json:"cpu"`     // e.g., "0.5"
+	Memory  string `json:"memory"`  // e.g., "512Mi"
+	Storage string `json:"storage"` // e.g., "1Gi"
+}
+
+type AutoscalingConfig struct {
+	Enabled      bool   `json:"enabled"`      // Whether autoscaling is enabled
+	MinReplicas  int    `json:"minReplicas"`  // Minimum number of replicas
+	MaxReplicas  int    `json:"maxReplicas"`  // Maximum number of replicas
+	CPUThreshold int    `json:"cpuThreshold"` // CPU threshold for scaling (percentage)
+	Metric       string `json:"metric"`       // Metric to base scaling on (e.g., "cpu", "memory")
+}
+
+type NetworkPolicy struct {
+	Name           string   `json:"name"`            // Policy name
+	FromComponents []string `json:"fromComponents"`  // Source components (renamed from FromApps)
+	ToComponents   []string `json:"toComponents"`    // Destination components (renamed from ToApps)
+	Ports          []int    `json:"ports,omitempty"` // Allowed ports
+	AllowEgress    bool     `json:"allowEgress"`     // Whether to allow egress traffic
+}
+
+type SecretConfig struct {
+	Name        string `json:"name"`        // Secret name
+	Description string `json:"description"` // Secret description
+	Required    bool   `json:"required"`    // Whether the secret is required
+}
+
+// BeforeSave serializes the embedded JSON fields
+func (a *App) BeforeSave(tx *gorm.DB) error {
+	// Marshal Config to JSON
+	configJSON, err := json.Marshal(a.Config)
+	if err != nil {
+		return err
+	}
+	a.ConfigJSON = string(configJSON)
+
+	return nil
+}
+
+// AfterFind deserializes the JSON fields
+func (a *App) AfterFind(tx *gorm.DB) error {
+	// Unmarshal Config from JSON
+	if a.ConfigJSON != "" {
+		if err := json.Unmarshal([]byte(a.ConfigJSON), &a.Config); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}

+ 8 - 8
models/deployment.go

@@ -37,7 +37,7 @@ type Deployment struct {
 	DeletedAt      gorm.DeletedAt `json:"-" gorm:"index"`                  // Soft delete support
 
 	// GORM relationships
-	DeployedApps []DeployedApp `json:"deployedApps" gorm:"foreignKey:DeploymentID"` // Array of deployed applications
+	DeployedComponents []DeployedComponent `json:"deployedComponents" gorm:"foreignKey:DeploymentID"` // Array of deployed components
 }
 
 // DeployedApp represents a specific app within a deployment
@@ -108,7 +108,7 @@ type AlertConfiguration struct {
 // DeploymentStatus type definitions
 type DeploymentStatus string
 type Environment string
-type AppDeploymentStatus string
+type ComponentDeploymentStatus string
 type HealthStatus string
 type AlertType string
 
@@ -126,12 +126,12 @@ const (
 	STAGING     Environment = "staging"
 	PRODUCTION  Environment = "production"
 
-	// AppDeploymentStatus values
-	PENDING_APP  AppDeploymentStatus = "pending"
-	RUNNING      AppDeploymentStatus = "running"
-	FAILED_APP   AppDeploymentStatus = "failed"
-	SCALING      AppDeploymentStatus = "scaling"
-	UPDATING_APP AppDeploymentStatus = "updating"
+	// ComponentDeploymentStatus values
+	PENDING_APP  ComponentDeploymentStatus = "pending"
+	RUNNING      ComponentDeploymentStatus = "running"
+	FAILED_APP   ComponentDeploymentStatus = "failed"
+	SCALING      ComponentDeploymentStatus = "scaling"
+	UPDATING_APP ComponentDeploymentStatus = "updating"
 
 	// HealthStatus values
 	HEALTHY   HealthStatus = "healthy"

+ 38 - 0
models/ovh/vps.go

@@ -0,0 +1,38 @@
+package models
+
+// OVHVPS represents the structure of a VPS from OVH API
+type OVHVPS struct {
+	Cluster     string `json:"cluster"`
+	DisplayName string `json:"displayName"`
+	IAM         struct {
+		DisplayName string            `json:"displayName"`
+		ID          string            `json:"id"`
+		Tags        map[string]string `json:"tags"`
+		URN         string            `json:"urn"`
+	} `json:"iam"`
+	Keymap             string   `json:"keymap"`
+	MemoryLimit        int      `json:"memoryLimit"`
+	Model              VPSModel `json:"model"`
+	MonitoringIpBlocks []string `json:"monitoringIpBlocks"`
+	Name               string   `json:"name"`
+	NetbootMode        string   `json:"netbootMode"`
+	OfferType          string   `json:"offerType"`
+	SLAMonitoring      bool     `json:"slaMonitoring"`
+	State              string   `json:"state"`
+	VCore              int      `json:"vcore"`
+	Zone               string   `json:"zone"`
+	ZoneType           string   `json:"zoneType"`
+}
+
+// VPSModel represents the model details of an OVH VPS
+type VPSModel struct {
+	AvailableOptions     []string `json:"availableOptions"`
+	Datacenter           []string `json:"datacenter"`
+	Disk                 int      `json:"disk"`
+	MaximumAdditionnalIp int      `json:"maximumAdditionnalIp"`
+	Memory               int      `json:"memory"`
+	Name                 string   `json:"name"`
+	Offer                string   `json:"offer"`
+	VCore                int      `json:"vcore"`
+	Version              string   `json:"version"`
+}

+ 153 - 0
services/blueprints.go

@@ -0,0 +1,153 @@
+package services
+
+import (
+	"fmt"
+
+	"git.linuxforward.com/byop/byop-engine/dbstore"
+	"git.linuxforward.com/byop/byop-engine/models"
+	"github.com/google/uuid"
+)
+
+// BlueprintService handles business logic for Blueprints
+type BlueprintService struct {
+	store *dbstore.BlueprintStore
+}
+
+// NewBlueprintService creates a new BlueprintService
+func NewBlueprintService(store *dbstore.BlueprintStore) *BlueprintService {
+	return &BlueprintService{store: store}
+}
+
+// CreateBlueprint creates a new deployment Blueprint
+func (s *BlueprintService) CreateBlueprint(Blueprint *models.Blueprint) error {
+	// Generate UUID if not provided
+	if Blueprint.ID == "" {
+		Blueprint.ID = uuid.New().String()
+	}
+
+	// Validate Blueprint configuration
+	if err := validateBlueprintConfig(Blueprint.Config); err != nil {
+		return fmt.Errorf("invalid Blueprint configuration: %w", err)
+	}
+
+	// Persist the Blueprint
+	return s.store.Create(Blueprint)
+}
+
+// GetBlueprint retrieves a Blueprint by ID
+func (s *BlueprintService) GetBlueprint(id string) (*models.Blueprint, error) {
+	Blueprint, err := s.store.GetByID(id)
+	if err != nil {
+		return nil, fmt.Errorf("failed to retrieve Blueprint: %w", err)
+	}
+	return Blueprint, nil
+}
+
+// UpdateBlueprint updates an existing Blueprint
+func (s *BlueprintService) UpdateBlueprint(Blueprint *models.Blueprint) error {
+	if Blueprint.ID == "" {
+		return fmt.Errorf("Blueprint ID is required for update")
+	}
+
+	// Check if Blueprint exists
+	existingBlueprint, err := s.store.GetByID(Blueprint.ID)
+	if err != nil {
+		return fmt.Errorf("failed to check if Blueprint exists: %w", err)
+	}
+	if existingBlueprint == nil {
+		return fmt.Errorf("Blueprint with ID %s not found", Blueprint.ID)
+	}
+
+	// Validate Blueprint configuration
+	if err := validateBlueprintConfig(Blueprint.Config); err != nil {
+		return fmt.Errorf("invalid Blueprint configuration: %w", err)
+	}
+
+	return s.store.Update(Blueprint)
+}
+
+// DeleteBlueprint deletes a Blueprint by ID
+func (s *BlueprintService) DeleteBlueprint(id string) error {
+	// Check if Blueprint exists
+	Blueprint, err := s.store.GetByID(id)
+	if err != nil {
+		return fmt.Errorf("failed to check if Blueprint exists: %w", err)
+	}
+	if Blueprint == nil {
+		return fmt.Errorf("Blueprint with ID %s not found", id)
+	}
+
+	// Check if the Blueprint has deployments
+	BlueprintWithDeployments, err := s.store.GetBlueprintWithDeployments(id)
+	if err != nil {
+		return fmt.Errorf("failed to check Blueprint deployments: %w", err)
+	}
+
+	// Don't allow deletion if there are active deployments
+	if len(BlueprintWithDeployments.Deployments) > 0 {
+		return fmt.Errorf("cannot delete Blueprint with active deployments")
+	}
+
+	return s.store.Delete(id)
+}
+
+// ListBlueprints retrieves all Blueprints with optional filtering
+func (s *BlueprintService) ListBlueprints(filter map[string]interface{}) ([]*models.Blueprint, error) {
+	return s.store.List(filter)
+}
+
+// GetBlueprintDeployments retrieves all deployments for a Blueprint
+func (s *BlueprintService) GetBlueprintDeployments(id string) ([]models.Deployment, error) {
+	// First check if the Blueprint exists
+	Blueprint, err := s.store.GetByID(id)
+	if err != nil {
+		return nil, fmt.Errorf("failed to check if Blueprint exists: %w", err)
+	}
+	if Blueprint == nil {
+		return nil, fmt.Errorf("Blueprint with ID %s not found", id)
+	}
+
+	// Get Blueprint with deployments
+	BlueprintWithDeployments, err := s.store.GetBlueprintWithDeployments(id)
+	if err != nil {
+		return nil, fmt.Errorf("failed to retrieve Blueprint deployments: %w", err)
+	}
+
+	return BlueprintWithDeployments.Deployments, nil
+}
+
+// GetBlueprintByVersion retrieves a Blueprint by name and version
+func (s *BlueprintService) GetBlueprintByVersion(name, version string) (*models.Blueprint, error) {
+	Blueprint, err := s.store.GetByVersion(name, version)
+	if err != nil {
+		return nil, fmt.Errorf("failed to retrieve Blueprint: %w", err)
+	}
+	return Blueprint, nil
+}
+
+// validateBlueprintConfig validates the Blueprint configuration
+func validateBlueprintConfig(config models.BlueprintConfig) error {
+	// Validate that at least one app is defined
+	if len(config.Components) == 0 {
+		return fmt.Errorf("Blueprint must define at least one app")
+	}
+
+	// Validate each app in the Blueprint
+	for i, app := range config.Components {
+		if app.Name == "" {
+			return fmt.Errorf("app at index %d must have a name", i)
+		}
+
+		// Validate resource configuration
+		if app.Resources.CPU == "" {
+			return fmt.Errorf("app '%s' must specify CPU resources", app.Name)
+		}
+		if app.Resources.Memory == "" {
+			return fmt.Errorf("app '%s' must specify memory resources", app.Name)
+		}
+	}
+
+	// Add additional validation logic as needed
+
+	return nil
+}

+ 99 - 18
services/deployments.go

@@ -1,10 +1,12 @@
 package services
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"time"
 
+	"git.linuxforward.com/byop/byop-engine/cloud"
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/models"
 )
@@ -44,21 +46,86 @@ func (s *DeploymentService) CreateDeployment(deployment *models.Deployment) erro
 
 	// Set timestamps
 	now := time.Now()
-	deployment.LastDeployedAt = now
+	deployment.CreatedAt = now
+	deployment.UpdatedAt = now
 
-	// Handle deployed apps setup
-	if err := s.setupDeployedApps(deployment); err != nil {
-		return fmt.Errorf("failed to setup deployed apps: %w", err)
+	// Get instance from pool
+	ctx := context.Background()
+	return s.createDeploymentWithNewInstance(ctx, deployment)
+}
+
+// createDeploymentWithNewInstance creates a new instance directly from the provider
+func (s *DeploymentService) createDeploymentWithNewInstance(ctx context.Context, deployment *models.Deployment) error {
+	// Get provider
+	provider, ok := cloud.GetProvider(deployment.Provider)
+	if !ok {
+		return fmt.Errorf("provider %s not found", deployment.Provider)
 	}
 
-	// Persist the deployment
-	if err := s.store.Create(deployment); err != nil {
-		return fmt.Errorf("failed to create deployment: %w", err)
+	blueprint, err := s.blueprintStore.GetByID(deployment.BlueprintID)
+	if err != nil {
+		return fmt.Errorf("failed to retrieve blueprint: %w", err)
 	}
 
-	// Trigger deployment process (this would normally be asynchronous)
-	// This is a placeholder for your actual deployment logic
-	go s.processDeployment(deployment.ID)
+	if blueprint == nil {
+		return fmt.Errorf("blueprint with ID %s not found", deployment.BlueprintID)
+	}
+
+	var components []models.Component
+	for _, component := range blueprint.Config.Components {
+		fmt.Printf("Component ID: %s\n", component.ID)
+		comp, err := s.componentStore.GetByID(component.ID)
+		if err != nil {
+			fmt.Errorf("failed to retrieve component: %w", err)
+			continue
+		}
+		if comp == nil {
+			fmt.Errorf("component with ID %s not found", component.ID)
+			continue
+		}
+
+		components = append(components, *comp)
+	}
+
+	// Create instance options
+	instanceOpts := cloud.InstanceCreateOpts{
+		Name:       fmt.Sprintf("deployment-%s", deployment.ID[:8]),
+		Components: components,
+		Tags: map[string]string{
+			"managed-by":    "byop-platform",
+			"deployment-id": deployment.ID,
+			"client-id":     deployment.ClientID,
+			// "component-id":  deployment.ComponentID,
+			"blueprint-id": deployment.BlueprintID,
+		},
+	}
+
+	// Create the instance
+	s.logger.WithFields(logrus.Fields{
+		"name":     instanceOpts.Name,
+		"provider": deployment.Provider,
+		"region":   deployment.Region,
+		"size":     deployment.InstanceType,
+	}).Info("Creating new instance for deployment")
+
+	instance, err := provider.GetFirstFreeInstance(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to create instance: %w", err)
+	}
+
+	// Update deployment with instance info
+	deployment.InstanceID = instance.ID
+	deployment.IPAddress = instance.IPAddress
+
+	// Save the deployment
+	if err := s.store.CreateDeployment(deployment); err != nil {
+		// If we fail to save deployment, try to delete the instance
+		_ = provider.ResetInstance(ctx, instance.ID)
+		return fmt.Errorf("failed to save deployment: %w", err)
+	}
+
+	// Start deployment process asynchronously
+	go s.processDeployment(ctx, deployment)
 
 	return nil
 }
@@ -99,7 +166,7 @@ func (s *DeploymentService) UpdateDeployment(deployment *models.Deployment) erro
 	// Prevent updates to deployed apps if deployment is not in the right state
 	if existingDeployment.Status != string(models.PENDING_DEPLOYMENT) &&
 		existingDeployment.Status != string(models.FAILED_DEPLOYMENT) &&
-		len(deployment.DeployedApps) > 0 {
+		len(deployment.DeployedComponents) > 0 {
 		return fmt.Errorf("cannot update deployed apps when deployment is in %s state", existingDeployment.Status)
 	}
 
@@ -115,7 +182,7 @@ func (s *DeploymentService) UpdateDeployment(deployment *models.Deployment) erro
 	}
 
 	// Handle deployed apps setup
-	if err := s.setupDeployedApps(deployment); err != nil {
+	if err := s.setupDeployedComponents(deployment); err != nil {
 		return fmt.Errorf("failed to setup deployed apps: %w", err)
 	}
 
@@ -295,10 +362,10 @@ func (s *DeploymentService) validateDeployment(deployment *models.Deployment) er
 	return nil
 }
 
-// setupDeployedApps sets up deployed apps based on the template
-func (s *DeploymentService) setupDeployedApps(deployment *models.Deployment) error {
+// setupDeployedComponents sets up deployed apps based on the blueprint
+func (s *DeploymentService) setupDeployedComponents(deployment *models.Deployment) error {
 	// If deployment already has deployed apps defined, we assume they're set up correctly
-	if len(deployment.DeployedApps) > 0 {
+	if len(deployment.DeployedComponents) > 0 {
 		return nil
 	}
 
@@ -323,6 +390,7 @@ func (s *DeploymentService) setupDeployedApps(deployment *models.Deployment) err
 		component, err := s.componentStore.GetByID(componentConfig.ID)
 		if err != nil {
 			return fmt.Errorf("failed to retrieve component: %w", err)
+			return fmt.Errorf("failed to retrieve component: %w", err)
 		}
 		if component == nil {
 			return fmt.Errorf("component with ID %d not found", componentConfig.ID)
@@ -332,20 +400,26 @@ func (s *DeploymentService) setupDeployedApps(deployment *models.Deployment) err
 		deployedApp := models.DeployedApp{
 			DeploymentID: deployment.ID,
 			ComponentID:  component.ID,
+			ComponentID:  component.ID,
 			Status:       string(models.PENDING_APP),
 			Version:      component.Version,
+			Version:      component.Version,
 			URL:          "", // Will be set during deployment
 			PodCount:     componentConfig.Autoscaling.MinReplicas,
+			PodCount:     componentConfig.Autoscaling.MinReplicas,
 			HealthStatus: string(models.HEALTHY),
 			Resources: models.ResourceAllocation{
 				CPU:     componentConfig.Resources.CPU,
 				Memory:  componentConfig.Resources.Memory,
 				Storage: componentConfig.Resources.Storage,
+				CPU:     componentConfig.Resources.CPU,
+				Memory:  componentConfig.Resources.Memory,
+				Storage: componentConfig.Resources.Storage,
 			},
 		}
 
 		// Add to deployment
-		deployment.DeployedApps = append(deployment.DeployedApps, deployedApp)
+		deployment.DeployedComponents = append(deployment.DeployedComponents, deployedComponent)
 	}
 
 	return nil
@@ -357,7 +431,7 @@ func (s *DeploymentService) processDeployment(deploymentID int64) {
 	// For now, we just update the status after a short delay to simulate the process
 
 	// Update status to deploying
-	_ = s.UpdateDeploymentStatus(deploymentID, string(models.DEPLOYING))
+	_ = s.UpdateDeploymentStatus(deployment.ID, string(models.DEPLOYING))
 
 	// In a real system, this would be where you'd:
 	// 1. Provision infrastructure
@@ -382,6 +456,8 @@ func (s *DeploymentService) processDeployment(deploymentID int64) {
 	} else {
 		_ = s.UpdateDeploymentStatus(deploymentID, string(models.FAILED_DEPLOYMENT))
 	}
+
+	s.logger.WithField("deployment_id", deployment.ID).Info("Deployment completed successfully")
 }
 
 // processDeploymentCleanup handles the cleanup process for deleted deployments
@@ -396,7 +472,12 @@ func (s *DeploymentService) processDeploymentCleanup(deploymentID int64) {
 	time.Sleep(2 * time.Second)
 
 	// Delete the deployment from the database
-	_ = s.store.Delete(deploymentID)
+	if err := s.store.Delete(deploymentID); err != nil {
+		s.logger.WithError(err).WithField("deployment_id", deploymentID).
+			Error("Failed to delete deployment record")
+	}
+
+	s.logger.WithField("deployment_id", deploymentID).Info("Deployment cleanup completed")
 }
 
 // deserializeConfigFields deserializes JSON config fields from strings