lblt hai 1 mes
pai
achega
6009e0299f

+ 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"]

+ 67 - 54
app/init.go

@@ -56,11 +56,11 @@ func (a *App) initCommonServices() error {
 	if err := a.dbManager.Migrate(
 		&models.User{},
 		&models.Client{},
-		&models.App{},
-		&models.Template{},
+		&models.Component{},
+		&models.Blueprint{},
 		&models.Deployment{},
-		&models.DeployedApp{},
-		&models.DeployedAppResource{},
+		&models.DeployedComponent{},
+		&models.DeployedComponentResource{},
 		// Add other models here
 	); err != nil {
 		return fmt.Errorf("migrate database: %w", err)
@@ -92,32 +92,32 @@ func (a *App) initHandlers() error {
 		Handler: clientHandler,
 	}
 
-	// Initialize AppModule
-	appStore := dbstore.NewAppStore(a.dbManager)
-	appService := services.NewAppService(appStore)
-	appHandler := handlers.NewAppHandler(appService)
-	a.appModule = &AppModule{
-		Store:   appStore,
-		Service: appService,
-		Handler: appHandler,
+	// Initialize ComponentModule
+	componentStore := dbstore.NewComponentStore(a.dbManager)
+	componentService := services.NewComponentService(componentStore)
+	componentHandler := handlers.NewComponentHandler(componentService)
+	a.componentModule = &ComponentModule{
+		Store:   componentStore,
+		Service: componentService,
+		Handler: componentHandler,
 	}
 
-	// Initialize TemplateModule
-	templateStore := dbstore.NewTemplateStore(a.dbManager)
-	templateService := services.NewTemplateService(templateStore)
-	templateHandler := handlers.NewTemplateHandler(templateService)
-	a.templateModule = &TemplateModule{
-		Store:   templateStore,
-		Service: templateService,
-		Handler: templateHandler,
+	// Initialize BlueprintModule
+	blueprintStore := dbstore.NewBlueprintStore(a.dbManager)
+	blueprintService := services.NewBlueprintService(blueprintStore)
+	blueprintHandler := handlers.NewBlueprintHandler(blueprintService)
+	a.blueprintModule = &BlueprintModule{
+		Store:   blueprintStore,
+		Service: blueprintService,
+		Handler: blueprintHandler,
 	}
 
 	// Initialize DeploymentModule
 	deploymentStore := dbstore.NewDeploymentStore(a.dbManager)
 	deploymentService := services.NewDeploymentService(
 		deploymentStore,
-		appStore,
-		templateStore,
+		componentStore,
+		blueprintStore,
 		clientStore,
 	)
 	deploymentHandler := handlers.NewDeploymentHandler(deploymentService)
@@ -138,20 +138,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
 }
 
@@ -181,44 +178,60 @@ 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)
+	clients.GET("", a.clientModule.Handler.ListClients)    // Without trailing slash
+	clients.GET("/", a.clientModule.Handler.ListClients)   // With trailing slash
+	clients.POST("", a.clientModule.Handler.CreateClient)  // Without trailing slash
+	clients.POST("/", a.clientModule.Handler.CreateClient) // With trailing slash
 	clients.GET("/:id", a.clientModule.Handler.GetClient)
 	clients.PUT("/:id", a.clientModule.Handler.UpdateClient)
 	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("", a.userModule.Handler.ListUsers)  // Without trailing slash
+	users.GET("/", a.userModule.Handler.ListUsers) // With trailing slash
 	users.GET("/:id", a.userModule.Handler.GetUser)
 	users.PUT("/:id", a.userModule.Handler.UpdateUser)
 	users.DELETE("/:id", a.userModule.Handler.DeleteUser)
 	users.GET("/:id/deployments", a.userModule.Handler.GetUserDeployments)
 
-	// App routes
-	apps := protected.Group("/apps")
-	apps.GET("/", a.appModule.Handler.ListApps)
-	apps.POST("/", a.appModule.Handler.CreateApp)
-	apps.GET("/:id", a.appModule.Handler.GetApp)
-	apps.PUT("/:id", a.appModule.Handler.UpdateApp)
-	apps.DELETE("/:id", a.appModule.Handler.DeleteApp)
-	apps.GET("/:id/deployments", a.appModule.Handler.GetAppDeployments)
-
-	// Template routes
-	templates := protected.Group("/templates")
-	templates.GET("/", a.templateModule.Handler.ListTemplates)
-	templates.POST("/", a.templateModule.Handler.CreateTemplate)
-	templates.GET("/:id", a.templateModule.Handler.GetTemplate)
-	templates.PUT("/:id", a.templateModule.Handler.UpdateTemplate)
-	templates.DELETE("/:id", a.templateModule.Handler.DeleteTemplate)
-	templates.GET("/:id/deployments", a.templateModule.Handler.GetTemplateDeployments)
-
-	// Deployment routes
+	// Component routes - registering both with and without trailing slash
+	components := protected.Group("/components")
+	components.GET("", a.componentModule.Handler.ListComponents)    // Without trailing slash
+	components.GET("/", a.componentModule.Handler.ListComponents)   // With trailing slash
+	components.POST("", a.componentModule.Handler.CreateComponent)  // Without trailing slash
+	components.POST("/", a.componentModule.Handler.CreateComponent) // With trailing slash
+	components.GET("/:id", a.componentModule.Handler.GetComponent)
+	components.PUT("/:id", a.componentModule.Handler.UpdateComponent)
+	components.DELETE("/:id", a.componentModule.Handler.DeleteComponent)
+	components.GET("/:id/deployments", a.componentModule.Handler.GetComponentDeployments)
+
+	// Blueprint routes - registering both with and without trailing slash
+	blueprints := protected.Group("/blueprints")
+	blueprints.GET("", a.blueprintModule.Handler.ListBlueprints)    // Without trailing slash
+	blueprints.GET("/", a.blueprintModule.Handler.ListBlueprints)   // With trailing slash
+	blueprints.POST("", a.blueprintModule.Handler.CreateBlueprint)  // Without trailing slash
+	blueprints.POST("/", a.blueprintModule.Handler.CreateBlueprint) // With trailing slash
+	blueprints.GET("/:id", a.blueprintModule.Handler.GetBlueprint)
+	blueprints.PUT("/:id", a.blueprintModule.Handler.UpdateBlueprint)
+	blueprints.DELETE("/:id", a.blueprintModule.Handler.DeleteBlueprint)
+	blueprints.GET("/:id/deployments", a.blueprintModule.Handler.GetBlueprintDeployments)
+
+	// Deployment routes - need to handle both versions
 	deployments := protected.Group("/deployments")
-	a.deploymentModule.Handler.RegisterRoutes(deployments)
+	deployments.GET("", a.deploymentModule.Handler.ListDeployments)
+	deployments.GET("/", a.deploymentModule.Handler.ListDeployments)
+	deployments.POST("", a.deploymentModule.Handler.CreateDeployment)
+	deployments.GET("/:id", a.deploymentModule.Handler.GetDeployment)
+	deployments.PUT("/:id", a.deploymentModule.Handler.UpdateDeployment)
+	deployments.DELETE("/:id", a.deploymentModule.Handler.DeleteDeployment)
+	deployments.PUT("/:id/status", a.deploymentModule.Handler.UpdateDeploymentStatus)
+	deployments.GET("/by-client/:clientId", a.deploymentModule.Handler.GetDeploymentsByClient)
+	deployments.GET("/by-blueprint/:blueprintId", a.deploymentModule.Handler.GetDeploymentsByBlueprint)
+	deployments.GET("/by-user/:userId", a.deploymentModule.Handler.GetDeploymentsByUser)
 
 	a.entry.Info("Routes configured successfully")
 }

+ 21 - 10
app/server.go

@@ -7,6 +7,7 @@ import (
 	"os"
 	"os/signal"
 	"strconv"
+	"sync"
 	"syscall"
 	"time"
 
@@ -39,13 +40,15 @@ type App struct {
 	authHandler      *handlers.AuthHandler
 	userModule       *UserModule
 	clientModule     *ClientModule
-	appModule        *AppModule
-	templateModule   *TemplateModule
+	componentModule  *ComponentModule // formerly appModule
+	blueprintModule  *BlueprintModule // formerly templateModule
 	deploymentModule *DeploymentModule
 
 	// Resource Handlers
 	providerHandler *handlers.ProviderHandler
 	// ticketHandler     *handlers.TicketHandler
+	stopped bool
+	wg      sync.WaitGroup
 	// monitoringHandler *handlers.MonitoringHandler
 }
 
@@ -61,16 +64,16 @@ type ClientModule struct {
 	Handler *handlers.ClientHandler
 }
 
-type AppModule struct {
-	Store   *dbstore.AppStore
-	Service *services.AppService
-	Handler *handlers.AppHandler
+type ComponentModule struct {
+	Store   *dbstore.ComponentStore
+	Service *services.ComponentService
+	Handler *handlers.ComponentHandler
 }
 
-type TemplateModule struct {
-	Store   *dbstore.TemplateStore
-	Service *services.TemplateService
-	Handler *handlers.TemplateHandler
+type BlueprintModule struct {
+	Store   *dbstore.BlueprintStore
+	Service *services.BlueprintService
+	Handler *handlers.BlueprintHandler
 }
 
 type DeploymentModule struct {
@@ -98,8 +101,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

BIN=BIN
byop-engine


+ 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

+ 26 - 28
config.sample.yml

@@ -2,45 +2,43 @@
 server:
   host: "0.0.0.0"  # Listen on all interfaces
   port: 8080       # HTTP port to listen on
-  tls:             # TLS/HTTPS configuration
-    enabled: false  # Set to true to enable HTTPS
-    cert_file: "/path/to/cert.pem"
-    key_file: "/path/to/key.pem"
+  tls:             
+    enabled: false  # TLS will be handled by Traefik
 
-# Database configuration
+# Database configuration - Using SQLite
 database:
-  host: "localhost"
-  port: 5432
-  username: "byop_user"
-  password: "secure_password"
-  name: "byop_db"
-  ssl_mode: "disable"  # Options: disable, require, verify-ca, verify-full
+  type: "sqlite"  # Database type
+  sqlite:
+    file: "/app/data/byop.db"  # Path inside the container
 
 # 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:-}"

+ 0 - 89
dbstore/app.go

@@ -1,89 +0,0 @@
-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"
-)
-
-// AppStore handles database operations for apps
-type AppStore struct {
-	db *gorm.DB
-}
-
-// NewAppStore creates a new AppStore
-func NewAppStore(dbManager dbmanager.DbManager) *AppStore {
-	return &AppStore{
-		db: dbManager.GetDB(),
-	}
-}
-
-// Create creates a new app
-func (as *AppStore) Create(app *models.App) error {
-	// Generate ID if not provided
-	if app.ID == "" {
-		app.ID = uuid.New().String()
-	}
-
-	// GORM will handle created_at and updated_at automatically
-	return as.db.Create(app).Error
-}
-
-// GetByID retrieves an app by ID
-func (as *AppStore) GetByID(id string) (*models.App, error) {
-	var app models.App
-	result := as.db.First(&app, "id = ?", id)
-	if result.Error != nil {
-		if result.Error == gorm.ErrRecordNotFound {
-			return nil, nil // No app found
-		}
-		return nil, fmt.Errorf("failed to get app: %w", result.Error)
-	}
-	return &app, nil
-}
-
-// Update updates an existing app
-func (as *AppStore) Update(app *models.App) error {
-	return as.db.Save(app).Error
-}
-
-// Delete deletes an app by ID
-func (as *AppStore) Delete(id string) error {
-	return as.db.Delete(&models.App{}, "id = ?", id).Error
-}
-
-// List retrieves all apps with optional filtering
-func (as *AppStore) List(filter map[string]interface{}) ([]*models.App, error) {
-	var apps []*models.App
-
-	// Build query from filters
-	query := as.db
-	if filter != nil {
-		for key, value := range filter {
-			query = query.Where(key+" = ?", value)
-		}
-	}
-
-	// Execute query
-	if err := query.Find(&apps).Error; err != nil {
-		return nil, fmt.Errorf("failed to list apps: %w", err)
-	}
-
-	return apps, nil
-}
-
-// GetAppWithDeployments retrieves an app by ID with associated deployments
-func (as *AppStore) GetAppWithDeployments(id string) (*models.App, error) {
-	var app models.App
-	result := as.db.Preload("Deployments").First(&app, "id = ?", id)
-	if result.Error != nil {
-		if result.Error == gorm.ErrRecordNotFound {
-			return nil, nil // No app found
-		}
-		return nil, fmt.Errorf("failed to get app: %w", result.Error)
-	}
-	return &app, nil
-}

+ 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
+}

+ 89 - 0
dbstore/component.go

@@ -0,0 +1,89 @@
+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"
+)
+
+// ComponentStore handles database operations for components
+type ComponentStore struct {
+	db *gorm.DB
+}
+
+// NewComponentStore creates a new ComponentStore
+func NewComponentStore(dbManager dbmanager.DbManager) *ComponentStore {
+	return &ComponentStore{
+		db: dbManager.GetDB(),
+	}
+}
+
+// Create creates a new component
+func (cs *ComponentStore) Create(component *models.Component) error {
+	// Generate ID if not provided
+	if component.ID == "" {
+		component.ID = uuid.New().String()
+	}
+
+	// GORM will handle created_at and updated_at automatically
+	return cs.db.Create(component).Error
+}
+
+// GetByID retrieves a component by ID
+func (cs *ComponentStore) GetByID(id string) (*models.Component, error) {
+	var component models.Component
+	result := cs.db.First(&component, "id = ?", id)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, nil // No component found
+		}
+		return nil, fmt.Errorf("failed to get component: %w", result.Error)
+	}
+	return &component, nil
+}
+
+// Update updates an existing component
+func (cs *ComponentStore) Update(component *models.Component) error {
+	return cs.db.Save(component).Error
+}
+
+// Delete deletes a component by ID
+func (cs *ComponentStore) Delete(id string) error {
+	return cs.db.Delete(&models.Component{}, "id = ?", id).Error
+}
+
+// List retrieves all components with optional filtering
+func (cs *ComponentStore) List(filter map[string]interface{}) ([]*models.Component, error) {
+	var components []*models.Component
+
+	// Build query from filters
+	query := cs.db
+	if filter != nil {
+		for key, value := range filter {
+			query = query.Where(key+" = ?", value)
+		}
+	}
+
+	// Execute query
+	if err := query.Find(&components).Error; err != nil {
+		return nil, fmt.Errorf("failed to list components: %w", err)
+	}
+
+	return components, nil
+}
+
+// GetComponentWithDeployments retrieves a component by ID with associated deployments
+func (cs *ComponentStore) GetComponentWithDeployments(id string) (*models.Component, error) {
+	var component models.Component
+	result := cs.db.Preload("Deployments").First(&component, "id = ?", id)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, nil // No component found
+		}
+		return nil, fmt.Errorf("failed to get component: %w", result.Error)
+	}
+	return &component, nil
+}

+ 51 - 51
dbstore/deployment.go

@@ -23,7 +23,7 @@ func NewDeploymentStore(dbManager dbmanager.DbManager) *DeploymentStore {
 }
 
 // Create creates a new deployment
-func (ds *DeploymentStore) Create(deployment *models.Deployment) error {
+func (ds *DeploymentStore) CreateDeployment(deployment *models.Deployment) error {
 	// Generate ID if not provided
 	if deployment.ID == "" {
 		deployment.ID = uuid.New().String()
@@ -42,8 +42,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 each deployed app has an ID
 			if app.ID == "" {
@@ -60,15 +60,15 @@ func (ds *DeploymentStore) Create(deployment *models.Deployment) error {
 
 			// Handle resources if provided
 			if app.Resources != (models.ResourceAllocation{}) {
-				resource := models.DeployedAppResource{
-					ID:            uuid.New().String(),
-					DeployedAppID: app.ID,
-					CPU:           app.Resources.CPU,
-					CPUUsage:      app.Resources.CPUUsage,
-					Memory:        app.Resources.Memory,
-					MemoryUsage:   app.Resources.MemoryUsage,
-					Storage:       app.Resources.Storage,
-					StorageUsage:  app.Resources.StorageUsage,
+				resource := models.DeployedComponentResource{
+					ID:                  uuid.New().String(),
+					DeployedComponentID: app.ID,
+					CPU:                 app.Resources.CPU,
+					CPUUsage:            app.Resources.CPUUsage,
+					Memory:              app.Resources.Memory,
+					MemoryUsage:         app.Resources.MemoryUsage,
+					Storage:             app.Resources.Storage,
+					StorageUsage:        app.Resources.StorageUsage,
 				}
 
 				if err := tx.Create(&resource).Error; err != nil {
@@ -87,7 +87,7 @@ func (ds *DeploymentStore) GetByID(id string) (*models.Deployment, error) {
 
 	// Get deployment with all related deployed apps
 	err := ds.db.
-		Preload("DeployedApps").
+		Preload("DeployedComponents").
 		First(&deployment, "id = ?", id).Error
 
 	if err != nil {
@@ -98,14 +98,14 @@ func (ds *DeploymentStore) GetByID(id string) (*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,
@@ -139,27 +139,27 @@ 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
 		}
 
 		// Create a map of existing app IDs for quick lookup
-		existingAppMap := make(map[string]bool)
-		for _, app := range existingApps {
-			existingAppMap[app.ID] = true
+		existingComponentMap := make(map[string]bool)
+		for _, app := range existingComponents {
+			existingComponentMap[app.ID] = true
 		}
 
 		// 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 != "" && existingAppMap[app.ID] {
+			if app.ID != "" && existingComponentMap[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
 				if app.ID == "" {
@@ -173,7 +173,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
@@ -181,15 +181,15 @@ func (ds *DeploymentStore) Update(deployment *models.Deployment) error {
 
 				if result.Error == gorm.ErrRecordNotFound {
 					// Create new resource
-					resource = models.DeployedAppResource{
-						ID:            uuid.New().String(),
-						DeployedAppID: app.ID,
-						CPU:           app.Resources.CPU,
-						CPUUsage:      app.Resources.CPUUsage,
-						Memory:        app.Resources.Memory,
-						MemoryUsage:   app.Resources.MemoryUsage,
-						Storage:       app.Resources.Storage,
-						StorageUsage:  app.Resources.StorageUsage,
+					resource = models.DeployedComponentResource{
+						ID:                  uuid.New().String(),
+						DeployedComponentID: app.ID,
+						CPU:                 app.Resources.CPU,
+						CPUUsage:            app.Resources.CPUUsage,
+						Memory:              app.Resources.Memory,
+						MemoryUsage:         app.Resources.MemoryUsage,
+						Storage:             app.Resources.Storage,
+						StorageUsage:        app.Resources.StorageUsage,
 					}
 					if err := tx.Create(&resource).Error; err != nil {
 						return err
@@ -210,12 +210,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
 			}
 		}
@@ -227,20 +227,20 @@ func (ds *DeploymentStore) Update(deployment *models.Deployment) error {
 // Delete deletes a deployment by ID
 func (ds *DeploymentStore) Delete(id string) 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
 		}
 
@@ -254,7 +254,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)
@@ -269,14 +269,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,
@@ -306,9 +306,9 @@ func (ds *DeploymentStore) GetByUserID(userID string) ([]*models.Deployment, err
 	return ds.List(map[string]interface{}{"created_by": userID})
 }
 
-// GetByTemplateID retrieves deployments based on a specific template
-func (ds *DeploymentStore) GetByTemplateID(templateID string) ([]*models.Deployment, error) {
-	return ds.List(map[string]interface{}{"template_id": templateID})
+// GetByBlueprintID retrieves deployments based on a specific blueprint
+func (ds *DeploymentStore) GetByBlueprintID(blueprintID string) ([]*models.Deployment, error) {
+	return ds.List(map[string]interface{}{"blueprint_id": blueprintID})
 }
 
 // serializeConfigFields serializes JSON config fields to strings

+ 0 - 102
dbstore/template.go

@@ -1,102 +0,0 @@
-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"
-)
-
-// TemplateStore handles database operations for deployment templates
-type TemplateStore struct {
-	db *gorm.DB
-}
-
-// NewTemplateStore creates a new TemplateStore
-func NewTemplateStore(dbManager dbmanager.DbManager) *TemplateStore {
-	return &TemplateStore{
-		db: dbManager.GetDB(),
-	}
-}
-
-// Create creates a new template
-func (ts *TemplateStore) Create(template *models.Template) error {
-	// Generate ID if not provided
-	if template.ID == "" {
-		template.ID = uuid.New().String()
-	}
-
-	// GORM will handle created_at and updated_at automatically
-	return ts.db.Create(template).Error
-}
-
-// GetByID retrieves a template by ID
-func (ts *TemplateStore) GetByID(id string) (*models.Template, error) {
-	var template models.Template
-	result := ts.db.First(&template, "id = ?", id)
-	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
-}
-
-// Update updates an existing template
-func (ts *TemplateStore) Update(template *models.Template) error {
-	return ts.db.Save(template).Error
-}
-
-// Delete deletes a template by ID
-func (ts *TemplateStore) Delete(id string) error {
-	return ts.db.Delete(&models.Template{}, "id = ?", id).Error
-}
-
-// List retrieves all templates with optional filtering
-func (ts *TemplateStore) List(filter map[string]interface{}) ([]*models.Template, error) {
-	var templates []*models.Template
-
-	// Build query from filters
-	query := ts.db
-	if filter != nil {
-		for key, value := range filter {
-			query = query.Where(key+" = ?", value)
-		}
-	}
-
-	// Execute query
-	if err := query.Find(&templates).Error; err != nil {
-		return nil, fmt.Errorf("failed to list templates: %w", err)
-	}
-
-	return templates, nil
-}
-
-// GetTemplateWithDeployments retrieves a template by ID with associated deployments
-func (ts *TemplateStore) GetTemplateWithDeployments(id string) (*models.Template, error) {
-	var template models.Template
-	result := ts.db.Preload("Deployments").First(&template, "id = ?", id)
-	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
-}
-
-// GetByVersion retrieves a template by name and version
-func (ts *TemplateStore) GetByVersion(name string, version string) (*models.Template, error) {
-	var template models.Template
-	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
-}

+ 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)
+}
+```

+ 0 - 138
handlers/apps.go

@@ -1,138 +0,0 @@
-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"
-)
-
-// AppHandler handles application-related operations
-type AppHandler struct {
-	service *services.AppService
-}
-
-// NewAppHandler creates a new AppHandler
-func NewAppHandler(service *services.AppService) *AppHandler {
-	return &AppHandler{
-		service: service,
-	}
-}
-
-// RegisterRoutes registers routes for app operations
-func (h *AppHandler) RegisterRoutes(r *gin.RouterGroup) {
-	r.GET("/", h.ListApps)
-	r.POST("/", h.CreateApp)
-	r.GET("/:id", h.GetApp)
-	r.PUT("/:id", h.UpdateApp)
-	r.DELETE("/:id", h.DeleteApp)
-	r.GET("/:id/deployments", h.GetAppDeployments)
-}
-
-// ListApps returns all applications with optional filtering
-func (h *AppHandler) ListApps(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
-	}
-
-	apps, err := h.service.ListApps(filter)
-	if err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to list applications: %v", err)})
-		return
-	}
-
-	c.JSON(http.StatusOK, apps)
-}
-
-// CreateApp creates a new application
-func (h *AppHandler) CreateApp(c *gin.Context) {
-	var app models.App
-
-	if err := c.ShouldBindJSON(&app); 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 {
-		app.CreatedBy = userID.(string)
-	}
-
-	if err := h.service.CreateApp(&app); err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create application: %v", err)})
-		return
-	}
-
-	c.JSON(http.StatusCreated, app)
-}
-
-// GetApp returns a specific application
-func (h *AppHandler) GetApp(c *gin.Context) {
-	id := c.Param("id")
-
-	app, err := h.service.GetApp(id)
-	if err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to fetch application: %v", err)})
-		return
-	}
-
-	if app == nil {
-		c.JSON(http.StatusNotFound, gin.H{"error": "Application not found"})
-		return
-	}
-
-	c.JSON(http.StatusOK, app)
-}
-
-// UpdateApp updates an application
-func (h *AppHandler) UpdateApp(c *gin.Context) {
-	id := c.Param("id")
-
-	var updatedApp models.App
-	if err := c.ShouldBindJSON(&updatedApp); err != nil {
-		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request body: %v", err)})
-		return
-	}
-
-	// Ensure the ID matches the URL parameter
-	updatedApp.ID = id
-
-	if err := h.service.UpdateApp(&updatedApp); err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update application: %v", err)})
-		return
-	}
-
-	c.JSON(http.StatusOK, updatedApp)
-}
-
-// DeleteApp deletes an application
-func (h *AppHandler) DeleteApp(c *gin.Context) {
-	id := c.Param("id")
-
-	if err := h.service.DeleteApp(id); err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to delete application: %v", err)})
-		return
-	}
-
-	c.Status(http.StatusNoContent)
-}
-
-// GetAppDeployments returns all deployments for an application
-func (h *AppHandler) GetAppDeployments(c *gin.Context) {
-	id := c.Param("id")
-
-	deployments, err := h.service.GetAppDeployments(id)
-	if err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to fetch application deployments: %v", err)})
-		return
-	}
-
-	c.JSON(http.StatusOK, deployments)
-}

+ 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)
+}

+ 138 - 0
handlers/components.go

@@ -0,0 +1,138 @@
+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"
+)
+
+// ComponentHandler handles component-related operations
+type ComponentHandler struct {
+	service *services.ComponentService
+}
+
+// NewComponentHandler creates a new ComponentHandler
+func NewComponentHandler(service *services.ComponentService) *ComponentHandler {
+	return &ComponentHandler{
+		service: service,
+	}
+}
+
+// RegisterRoutes registers routes for component operations
+func (h *ComponentHandler) RegisterRoutes(r *gin.RouterGroup) {
+	r.GET("/", h.ListComponents)
+	r.POST("/", h.CreateComponent)
+	r.GET("/:id", h.GetComponent)
+	r.PUT("/:id", h.UpdateComponent)
+	r.DELETE("/:id", h.DeleteComponent)
+	r.GET("/:id/deployments", h.GetComponentDeployments)
+}
+
+// ListComponents returns all components with optional filtering
+func (h *ComponentHandler) ListComponents(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
+	}
+
+	components, err := h.service.ListComponents(filter)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to list components: %v", err)})
+		return
+	}
+
+	c.JSON(http.StatusOK, components)
+}
+
+// CreateComponent creates a new component
+func (h *ComponentHandler) CreateComponent(c *gin.Context) {
+	var component models.Component
+
+	if err := c.ShouldBindJSON(&component); 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 {
+		component.CreatedBy = userID.(string)
+	}
+
+	if err := h.service.CreateComponent(&component); err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create component: %v", err)})
+		return
+	}
+
+	c.JSON(http.StatusCreated, component)
+}
+
+// GetComponent returns a specific component
+func (h *ComponentHandler) GetComponent(c *gin.Context) {
+	id := c.Param("id")
+
+	component, err := h.service.GetComponent(id)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to fetch component: %v", err)})
+		return
+	}
+
+	if component == nil {
+		c.JSON(http.StatusNotFound, gin.H{"error": "Component not found"})
+		return
+	}
+
+	c.JSON(http.StatusOK, component)
+}
+
+// UpdateComponent updates a component
+func (h *ComponentHandler) UpdateComponent(c *gin.Context) {
+	id := c.Param("id")
+
+	var updatedComponent models.Component
+	if err := c.ShouldBindJSON(&updatedComponent); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request body: %v", err)})
+		return
+	}
+
+	// Ensure the ID matches the URL parameter
+	updatedComponent.ID = id
+
+	if err := h.service.UpdateComponent(&updatedComponent); err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update component: %v", err)})
+		return
+	}
+
+	c.JSON(http.StatusOK, updatedComponent)
+}
+
+// DeleteComponent deletes a component
+func (h *ComponentHandler) DeleteComponent(c *gin.Context) {
+	id := c.Param("id")
+
+	if err := h.service.DeleteComponent(id); err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to delete component: %v", err)})
+		return
+	}
+
+	c.Status(http.StatusNoContent)
+}
+
+// GetComponentDeployments returns all deployments for a component
+func (h *ComponentHandler) GetComponentDeployments(c *gin.Context) {
+	id := c.Param("id")
+
+	deployments, err := h.service.GetComponentDeployments(id)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to fetch component deployments: %v", err)})
+		return
+	}
+
+	c.JSON(http.StatusOK, deployments)
+}

+ 6 - 6
handlers/deployments.go

@@ -30,7 +30,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)
 }
 
@@ -161,13 +161,13 @@ func (h *DeploymentHandler) GetDeploymentsByClient(c *gin.Context) {
 	c.JSON(http.StatusOK, deployments)
 }
 
-// GetDeploymentsByTemplate returns all deployments for a specific template
-func (h *DeploymentHandler) GetDeploymentsByTemplate(c *gin.Context) {
-	templateID := c.Param("templateId")
+// GetDeploymentsByBlueprint returns all deployments for a specific blueprint
+func (h *DeploymentHandler) GetDeploymentsByBlueprint(c *gin.Context) {
+	blueprintID := c.Param("blueprintId")
 
-	deployments, err := h.service.GetDeploymentsByTemplateID(templateID)
+	deployments, err := h.service.GetDeploymentsByBlueprintID(blueprintID)
 	if err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to fetch template deployments: %v", err)})
+		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to fetch blueprint deployments: %v", err)})
 		return
 	}
 

+ 0 - 152
handlers/templates.go

@@ -1,152 +0,0 @@
-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"
-)
-
-// TemplateHandler handles template-related operations
-type TemplateHandler struct {
-	service *services.TemplateService
-}
-
-// NewTemplateHandler creates a new TemplateHandler
-func NewTemplateHandler(service *services.TemplateService) *TemplateHandler {
-	return &TemplateHandler{
-		service: service,
-	}
-}
-
-// ListTemplates returns all templates with optional filtering
-func (h *TemplateHandler) ListTemplates(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
-	}
-
-	templates, err := h.service.ListTemplates(filter)
-	if err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to list templates: %v", err)})
-		return
-	}
-
-	c.JSON(http.StatusOK, templates)
-}
-
-// CreateTemplate creates a new deployment template
-func (h *TemplateHandler) CreateTemplate(c *gin.Context) {
-	var template models.Template
-
-	if err := c.ShouldBindJSON(&template); 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 {
-		template.CreatedBy = userID.(string)
-	}
-
-	if err := h.service.CreateTemplate(&template); err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create template: %v", err)})
-		return
-	}
-
-	c.JSON(http.StatusCreated, template)
-}
-
-// GetTemplate returns a specific template
-func (h *TemplateHandler) GetTemplate(c *gin.Context) {
-	id := c.Param("id")
-
-	template, err := h.service.GetTemplate(id)
-	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": "Template not found"})
-		return
-	}
-
-	c.JSON(http.StatusOK, template)
-}
-
-// UpdateTemplate updates a template
-func (h *TemplateHandler) UpdateTemplate(c *gin.Context) {
-	id := c.Param("id")
-
-	var updatedTemplate models.Template
-	if err := c.ShouldBindJSON(&updatedTemplate); err != nil {
-		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request body: %v", err)})
-		return
-	}
-
-	// Ensure the ID matches the URL parameter
-	updatedTemplate.ID = id
-
-	if err := h.service.UpdateTemplate(&updatedTemplate); err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update template: %v", err)})
-		return
-	}
-
-	c.JSON(http.StatusOK, updatedTemplate)
-}
-
-// DeleteTemplate deletes a template
-func (h *TemplateHandler) DeleteTemplate(c *gin.Context) {
-	id := c.Param("id")
-
-	if err := h.service.DeleteTemplate(id); err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to delete template: %v", err)})
-		return
-	}
-
-	c.Status(http.StatusNoContent)
-}
-
-// GetTemplateDeployments returns all deployments for a template
-func (h *TemplateHandler) GetTemplateDeployments(c *gin.Context) {
-	id := c.Param("id")
-
-	deployments, err := h.service.GetTemplateDeployments(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)
-}
-
-// GetTemplateByVersion handles retrieval of a template by name and version
-func (h *TemplateHandler) GetTemplateByVersion(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.GetTemplateByVersion(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": "Template not found"})
-		return
-	}
-
-	c.JSON(http.StatusOK, template)
-}

+ 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()
+	}
+}

+ 21 - 21
models/template.go → models/blueprint.go

@@ -7,7 +7,7 @@ import (
 	"gorm.io/gorm"
 )
 
-type Template struct {
+type Blueprint struct {
 	ID          string `json:"id" gorm:"primaryKey"`
 	Name        string `json:"name" gorm:"not null"`
 	Description string `json:"description"`
@@ -17,29 +17,29 @@ type Template struct {
 	ConfigJSON string `json:"-" gorm:"column:config;type:text"`
 
 	// Virtual field for ORM serialization/deserialization
-	Config TemplateConfig `json:"config" gorm:"-"`
+	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 template
+	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:TemplateID"` // Deployments using this template
+	Deployments []Deployment `json:"deployments" gorm:"foreignKey:BlueprintID"` // Deployments using this blueprint
 }
 
-type TemplateConfig struct {
-	Apps            []AppConfig       `json:"apps"`                   // Apps included in this template
+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 AppConfig struct {
-	ID           string            `json:"id"`                     // Reference to the app
-	Name         string            `json:"name"`                   // Name of the app in this template
+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 app is publicly accessible
+	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
@@ -61,11 +61,11 @@ type AutoscalingConfig struct {
 }
 
 type NetworkPolicy struct {
-	Name        string   `json:"name"`            // Policy name
-	FromApps    []string `json:"fromApps"`        // Source apps
-	ToApps      []string `json:"toApps"`          // Destination apps
-	Ports       []int    `json:"ports,omitempty"` // Allowed ports
-	AllowEgress bool     `json:"allowEgress"`     // Whether to allow egress traffic
+	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 {
@@ -75,22 +75,22 @@ type SecretConfig struct {
 }
 
 // BeforeSave serializes the embedded JSON fields
-func (t *Template) BeforeSave(tx *gorm.DB) error {
+func (b *Blueprint) BeforeSave(tx *gorm.DB) error {
 	// Marshal Config to JSON
-	configJSON, err := json.Marshal(t.Config)
+	configJSON, err := json.Marshal(b.Config)
 	if err != nil {
 		return err
 	}
-	t.ConfigJSON = string(configJSON)
+	b.ConfigJSON = string(configJSON)
 
 	return nil
 }
 
 // AfterFind deserializes the JSON fields
-func (t *Template) AfterFind(tx *gorm.DB) error {
+func (b *Blueprint) AfterFind(tx *gorm.DB) error {
 	// Unmarshal Config from JSON
-	if t.ConfigJSON != "" {
-		if err := json.Unmarshal([]byte(t.ConfigJSON), &t.Config); err != nil {
+	if b.ConfigJSON != "" {
+		if err := json.Unmarshal([]byte(b.ConfigJSON), &b.Config); err != nil {
 			return err
 		}
 	}

+ 26 - 26
models/apps.go → models/component.go

@@ -7,13 +7,13 @@ import (
 	"gorm.io/gorm"
 )
 
-type App struct {
-	ID          string `json:"id" gorm:"primaryKey"`
-	Name        string `json:"name" gorm:"not null"`
-	Description string `json:"description"`
-	Type        string `json:"type" gorm:"index"` // frontend, backend, api, database, or microservice
-	Language    string `json:"language"`          // Programming language or framework
-	Version     string `json:"version"`           // Version number (e.g., 1.0.0)
+type Component struct {
+	ID          string        `json:"id" gorm:"primaryKey"`
+	Name        string        `json:"name" gorm:"not null"`
+	Description string        `json:"description"`
+	Type        ComponentType `json:"type" gorm:"index"` // frontend, backend, api, database, or microservice
+	Language    string        `json:"language"`          // Programming language or framework
+	Version     string        `json:"version"`           // Version number (e.g., 1.0.0)
 
 	// Configuration details
 	ConfigFile   string `json:"configFile" gorm:"type:text"`   // JSON configuration as a string
@@ -34,11 +34,11 @@ type App struct {
 
 	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 app
+	CreatedBy string         `json:"createdBy" gorm:"index"` // User ID who created the component
 	DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`         // Soft delete support
 
 	// Relationships
-	Deployments []DeployedApp `json:"deployments" gorm:"foreignKey:AppID"` // Apps deployed in deployments
+	Deployments []DeployedComponent `json:"deployments" gorm:"foreignKey:ComponentID"` // Components deployed in deployments
 }
 
 type ResourceRequirements struct {
@@ -53,50 +53,50 @@ type ScaleSettings struct {
 	CPUThreshold int `json:"cpuThreshold"` // CPU threshold for scaling (percentage)
 }
 
-// AppType represents the type of application
-type AppType string
+// ComponentType represents the type of component
+type ComponentType string
 
 const (
-	Frontend     AppType = "frontend"
-	Backend      AppType = "backend"
-	API          AppType = "api"
-	Database     AppType = "database"
-	Microservice AppType = "microservice"
+	Frontend     ComponentType = "frontend"
+	Backend      ComponentType = "backend"
+	API          ComponentType = "api"
+	Database     ComponentType = "database"
+	Microservice ComponentType = "microservice"
 )
 
 // BeforeSave serializes the embedded JSON fields
-func (a *App) BeforeSave(tx *gorm.DB) error {
+func (c *Component) BeforeSave(tx *gorm.DB) error {
 	var err error
 
 	// Marshal Resources to JSON
-	resourcesJSON, err := json.Marshal(a.Resources)
+	resourcesJSON, err := json.Marshal(c.Resources)
 	if err != nil {
 		return err
 	}
-	a.ResourcesJSON = string(resourcesJSON)
+	c.ResourcesJSON = string(resourcesJSON)
 
 	// Marshal ScaleSettings to JSON
-	scaleSettingsJSON, err := json.Marshal(a.ScaleSettings)
+	scaleSettingsJSON, err := json.Marshal(c.ScaleSettings)
 	if err != nil {
 		return err
 	}
-	a.ScaleSettingsJSON = string(scaleSettingsJSON)
+	c.ScaleSettingsJSON = string(scaleSettingsJSON)
 
 	return nil
 }
 
 // AfterFind deserializes the JSON fields
-func (a *App) AfterFind(tx *gorm.DB) error {
+func (c *Component) AfterFind(tx *gorm.DB) error {
 	// Unmarshal Resources from JSON
-	if a.ResourcesJSON != "" {
-		if err := json.Unmarshal([]byte(a.ResourcesJSON), &a.Resources); err != nil {
+	if c.ResourcesJSON != "" {
+		if err := json.Unmarshal([]byte(c.ResourcesJSON), &c.Resources); err != nil {
 			return err
 		}
 	}
 
 	// Unmarshal ScaleSettings from JSON
-	if a.ScaleSettingsJSON != "" {
-		if err := json.Unmarshal([]byte(a.ScaleSettingsJSON), &a.ScaleSettings); err != nil {
+	if c.ScaleSettingsJSON != "" {
+		if err := json.Unmarshal([]byte(c.ScaleSettingsJSON), &c.ScaleSettings); err != nil {
 			return err
 		}
 	}

+ 32 - 24
models/deployment.go

@@ -13,8 +13,16 @@ type Deployment struct {
 	Description string `json:"description"`          // Deployment description
 
 	// Core relationships
-	TemplateID string `json:"templateId" gorm:"index"` // Reference to the template being deployed
-	ClientID   string `json:"clientId" gorm:"index"`   // Client this deployment belongs to
+	BlueprintID string `json:"blueprintId" gorm:"index"` // Reference to the blueprint being deployed
+	ClientID    string `json:"clientId" gorm:"index"`    // Client this deployment belongs to
+
+	// Infrastructure details
+	Provider     string `json:"provider"`                        // Cloud provider (aws, gcp, azure, etc.)
+	InstanceID   string `json:"instanceId"`                      // ID of the provisioned instance
+	InstanceType string `json:"instanceType"`                    // Type/size of instance
+	ImageID      string `json:"imageId"`                         // Image ID used for the instance
+	IPAddress    string `json:"ipAddress"`                       // IP address of the instance
+	IsFromPool   bool   `json:"isFromPool" gorm:"default:false"` // Whether this instance came from the pool
 
 	// Status and environment
 	Status      string `json:"status" gorm:"default:'pending'"`          // Current deployment status
@@ -37,14 +45,14 @@ 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
-type DeployedApp struct {
+// DeployedComponent represents a specific component within a deployment
+type DeployedComponent struct {
 	ID             string         `json:"id" gorm:"primaryKey"`                  // Unique identifier
 	DeploymentID   string         `json:"deploymentId" gorm:"index"`             // Reference to the parent deployment
-	AppID          string         `json:"appId" gorm:"index"`                    // Reference to the app being deployed
+	ComponentID    string         `json:"componentId" gorm:"index"`              // Reference to the component being deployed
 	Status         string         `json:"status" gorm:"default:'pending'"`       // Status of this specific app's deployment
 	Version        string         `json:"version"`                               // Deployed version
 	URL            string         `json:"url"`                                   // URL to access this app
@@ -59,17 +67,17 @@ type DeployedApp struct {
 	Resources ResourceAllocation `json:"resources" gorm:"-"` // Actual resources allocated
 }
 
-// App resource allocation (will be stored in DeployedAppResource table)
-type DeployedAppResource struct {
-	ID            string    `json:"id" gorm:"primaryKey"`              // Unique identifier
-	DeployedAppID string    `json:"deployedAppId" gorm:"uniqueIndex"`  // Reference to deployed app
-	CPU           string    `json:"cpu"`                               // Allocated CPU
-	CPUUsage      float64   `json:"cpuUsage"`                          // Current CPU usage percentage
-	Memory        string    `json:"memory"`                            // Allocated memory
-	MemoryUsage   float64   `json:"memoryUsage"`                       // Current memory usage percentage
-	Storage       string    `json:"storage"`                           // Allocated storage
-	StorageUsage  float64   `json:"storageUsage"`                      // Current storage usage percentage
-	LastUpdated   time.Time `json:"lastUpdated" gorm:"autoUpdateTime"` // When metrics were last updated
+// Component resource allocation (will be stored in DeployedComponentResource table)
+type DeployedComponentResource struct {
+	ID                  string    `json:"id" gorm:"primaryKey"`                   // Unique identifier
+	DeployedComponentID string    `json:"deployedComponentId" gorm:"uniqueIndex"` // Reference to deployed component
+	CPU                 string    `json:"cpu"`                                    // Allocated CPU
+	CPUUsage            float64   `json:"cpuUsage"`                               // Current CPU usage percentage
+	Memory              string    `json:"memory"`                                 // Allocated memory
+	MemoryUsage         float64   `json:"memoryUsage"`                            // Current memory usage percentage
+	Storage             string    `json:"storage"`                                // Allocated storage
+	StorageUsage        float64   `json:"storageUsage"`                           // Current storage usage percentage
+	LastUpdated         time.Time `json:"lastUpdated" gorm:"autoUpdateTime"`      // When metrics were last updated
 }
 
 // For backward compatibility
@@ -108,7 +116,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 +134,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"`
+}

+ 0 - 118
services/apps.go

@@ -1,118 +0,0 @@
-package services
-
-import (
-	"fmt"
-
-	"git.linuxforward.com/byop/byop-engine/dbstore"
-	"git.linuxforward.com/byop/byop-engine/models"
-	"github.com/google/uuid"
-)
-
-// AppService handles business logic for applications
-type AppService struct {
-	store *dbstore.AppStore
-}
-
-// NewAppService creates a new AppService
-func NewAppService(store *dbstore.AppStore) *AppService {
-	return &AppService{store: store}
-}
-
-// CreateApp creates a new application
-func (s *AppService) CreateApp(app *models.App) error {
-	// Generate UUID if not provided
-	if app.ID == "" {
-		app.ID = uuid.New().String()
-	}
-
-	// Set default resource values if not provided
-	if app.Resources.CPU == "" {
-		app.Resources.CPU = "0.5"
-	}
-	if app.Resources.Memory == "" {
-		app.Resources.Memory = "512Mi"
-	}
-	if app.Resources.Storage == "" {
-		app.Resources.Storage = "1Gi"
-	}
-
-	// Set default scale settings if not provided
-	if app.ScaleSettings.MinInstances == 0 {
-		app.ScaleSettings.MinInstances = 1
-	}
-	if app.ScaleSettings.MaxInstances == 0 {
-		app.ScaleSettings.MaxInstances = 3
-	}
-	if app.ScaleSettings.CPUThreshold == 0 {
-		app.ScaleSettings.CPUThreshold = 80
-	}
-
-	// Persist the app
-	return s.store.Create(app)
-}
-
-// GetApp retrieves an application by ID
-func (s *AppService) GetApp(id string) (*models.App, error) {
-	app, err := s.store.GetByID(id)
-	if err != nil {
-		return nil, fmt.Errorf("failed to retrieve app: %w", err)
-	}
-	return app, nil
-}
-
-// UpdateApp updates an existing application
-func (s *AppService) UpdateApp(app *models.App) error {
-	if app.ID == "" {
-		return fmt.Errorf("app ID is required for update")
-	}
-
-	// Check if app exists
-	existingApp, err := s.store.GetByID(app.ID)
-	if err != nil {
-		return fmt.Errorf("failed to check if app exists: %w", err)
-	}
-	if existingApp == nil {
-		return fmt.Errorf("app with ID %s not found", app.ID)
-	}
-
-	return s.store.Update(app)
-}
-
-// DeleteApp deletes an application by ID
-func (s *AppService) DeleteApp(id string) error {
-	// Check if app exists
-	app, err := s.store.GetByID(id)
-	if err != nil {
-		return fmt.Errorf("failed to check if app exists: %w", err)
-	}
-	if app == nil {
-		return fmt.Errorf("app with ID %s not found", id)
-	}
-
-	return s.store.Delete(id)
-}
-
-// ListApps retrieves all applications with optional filtering
-func (s *AppService) ListApps(filter map[string]interface{}) ([]*models.App, error) {
-	return s.store.List(filter)
-}
-
-// GetAppDeployments retrieves all deployments for an application
-func (s *AppService) GetAppDeployments(id string) ([]models.DeployedApp, error) {
-	// First check if the app exists
-	app, err := s.store.GetByID(id)
-	if err != nil {
-		return nil, fmt.Errorf("failed to check if app exists: %w", err)
-	}
-	if app == nil {
-		return nil, fmt.Errorf("app with ID %s not found", id)
-	}
-
-	// Get app with deployments
-	appWithDeployments, err := s.store.GetAppWithDeployments(id)
-	if err != nil {
-		return nil, fmt.Errorf("failed to retrieve app deployments: %w", err)
-	}
-
-	return appWithDeployments.Deployments, nil
-}

+ 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
+}

+ 118 - 0
services/components.go

@@ -0,0 +1,118 @@
+package services
+
+import (
+	"fmt"
+
+	"git.linuxforward.com/byop/byop-engine/dbstore"
+	"git.linuxforward.com/byop/byop-engine/models"
+	"github.com/google/uuid"
+)
+
+// ComponentService handles business logic for components
+type ComponentService struct {
+	store *dbstore.ComponentStore
+}
+
+// NewComponentService creates a new ComponentService
+func NewComponentService(store *dbstore.ComponentStore) *ComponentService {
+	return &ComponentService{store: store}
+}
+
+// CreateComponent creates a new component
+func (s *ComponentService) CreateComponent(component *models.Component) error {
+	// Generate UUID if not provided
+	if component.ID == "" {
+		component.ID = uuid.New().String()
+	}
+
+	// Set default resource values if not provided
+	if component.Resources.CPU == "" {
+		component.Resources.CPU = "0.5"
+	}
+	if component.Resources.Memory == "" {
+		component.Resources.Memory = "512Mi"
+	}
+	if component.Resources.Storage == "" {
+		component.Resources.Storage = "1Gi"
+	}
+
+	// Set default scale settings if not provided
+	if component.ScaleSettings.MinInstances == 0 {
+		component.ScaleSettings.MinInstances = 1
+	}
+	if component.ScaleSettings.MaxInstances == 0 {
+		component.ScaleSettings.MaxInstances = 3
+	}
+	if component.ScaleSettings.CPUThreshold == 0 {
+		component.ScaleSettings.CPUThreshold = 80
+	}
+
+	// Persist the component
+	return s.store.Create(component)
+}
+
+// GetComponent retrieves a component by ID
+func (s *ComponentService) GetComponent(id string) (*models.Component, error) {
+	component, err := s.store.GetByID(id)
+	if err != nil {
+		return nil, fmt.Errorf("failed to retrieve component: %w", err)
+	}
+	return component, nil
+}
+
+// UpdateComponent updates an existing component
+func (s *ComponentService) UpdateComponent(component *models.Component) error {
+	if component.ID == "" {
+		return fmt.Errorf("component ID is required for update")
+	}
+
+	// Check if component exists
+	existingComponent, err := s.store.GetByID(component.ID)
+	if err != nil {
+		return fmt.Errorf("failed to check if component exists: %w", err)
+	}
+	if existingComponent == nil {
+		return fmt.Errorf("component with ID %s not found", component.ID)
+	}
+
+	return s.store.Update(component)
+}
+
+// DeleteComponent deletes a component by ID
+func (s *ComponentService) DeleteComponent(id string) error {
+	// Check if component exists
+	component, err := s.store.GetByID(id)
+	if err != nil {
+		return fmt.Errorf("failed to check if component exists: %w", err)
+	}
+	if component == nil {
+		return fmt.Errorf("component with ID %s not found", id)
+	}
+
+	return s.store.Delete(id)
+}
+
+// ListComponents retrieves all components with optional filtering
+func (s *ComponentService) ListComponents(filter map[string]interface{}) ([]*models.Component, error) {
+	return s.store.List(filter)
+}
+
+// GetComponentDeployments retrieves all deployments for a component
+func (s *ComponentService) GetComponentDeployments(id string) ([]models.DeployedComponent, error) {
+	// First check if the component exists
+	component, err := s.store.GetByID(id)
+	if err != nil {
+		return nil, fmt.Errorf("failed to check if component exists: %w", err)
+	}
+	if component == nil {
+		return nil, fmt.Errorf("component with ID %s not found", id)
+	}
+
+	// Get component with deployments
+	componentWithDeployments, err := s.store.GetComponentWithDeployments(id)
+	if err != nil {
+		return nil, fmt.Errorf("failed to retrieve component deployments: %w", err)
+	}
+
+	return componentWithDeployments.Deployments, nil
+}

+ 190 - 90
services/deployments.go

@@ -1,35 +1,40 @@
 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"
 	"github.com/google/uuid"
+	"github.com/sirupsen/logrus"
 )
 
 // DeploymentService handles business logic for deployments
 type DeploymentService struct {
-	store         *dbstore.DeploymentStore
-	appStore      *dbstore.AppStore
-	templateStore *dbstore.TemplateStore
-	clientStore   *dbstore.ClientStore
+	store          *dbstore.DeploymentStore
+	componentStore *dbstore.ComponentStore
+	blueprintStore *dbstore.BlueprintStore
+	clientStore    *dbstore.ClientStore
+	logger         *logrus.Entry
 }
 
 // NewDeploymentService creates a new DeploymentService
 func NewDeploymentService(
 	store *dbstore.DeploymentStore,
-	appStore *dbstore.AppStore,
-	templateStore *dbstore.TemplateStore,
+	componentStore *dbstore.ComponentStore,
+	blueprintStore *dbstore.BlueprintStore,
 	clientStore *dbstore.ClientStore,
 ) *DeploymentService {
 	return &DeploymentService{
-		store:         store,
-		appStore:      appStore,
-		templateStore: templateStore,
-		clientStore:   clientStore,
+		store:          store,
+		componentStore: componentStore,
+		blueprintStore: blueprintStore,
+		clientStore:    clientStore,
+		logger:         logrus.WithField("component", "deployment_service"),
 	}
 }
 
@@ -50,21 +55,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)
+	}
+
+	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)
 	}
 
-	// Trigger deployment process (this would normally be asynchronous)
-	// This is a placeholder for your actual deployment logic
-	go s.processDeployment(deployment.ID)
+	// 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
 }
@@ -105,7 +175,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)
 	}
 
@@ -121,7 +191,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)
 	}
 
@@ -217,20 +287,20 @@ func (s *DeploymentService) GetDeploymentsByUserID(userID string) ([]*models.Dep
 	return deployments, nil
 }
 
-// GetDeploymentsByTemplateID retrieves deployments based on a specific template
-func (s *DeploymentService) GetDeploymentsByTemplateID(templateID string) ([]*models.Deployment, error) {
-	// Check if template exists
-	template, err := s.templateStore.GetByID(templateID)
+// GetDeploymentsByBlueprintID retrieves deployments based on a specific blueprint
+func (s *DeploymentService) GetDeploymentsByBlueprintID(blueprintID string) ([]*models.Deployment, error) {
+	// Check if blueprint exists
+	blueprint, err := s.blueprintStore.GetByID(blueprintID)
 	if err != nil {
-		return nil, fmt.Errorf("failed to check if template exists: %w", err)
+		return nil, fmt.Errorf("failed to check if blueprint exists: %w", err)
 	}
-	if template == nil {
-		return nil, fmt.Errorf("template with ID %s not found", templateID)
+	if blueprint == nil {
+		return nil, fmt.Errorf("blueprint with ID %s not found", blueprintID)
 	}
 
-	deployments, err := s.store.GetByTemplateID(templateID)
+	deployments, err := s.store.GetByBlueprintID(blueprintID)
 	if err != nil {
-		return nil, fmt.Errorf("failed to retrieve deployments for template %s: %w", templateID, err)
+		return nil, fmt.Errorf("failed to retrieve deployments for blueprint %s: %w", blueprintID, err)
 	}
 
 	// Deserialize config fields for each deployment
@@ -287,116 +357,146 @@ func (s *DeploymentService) validateDeployment(deployment *models.Deployment) er
 		return fmt.Errorf("client with ID %s not found", deployment.ClientID)
 	}
 
-	if deployment.TemplateID == "" {
-		return fmt.Errorf("template ID is required")
+	if deployment.BlueprintID == "" {
+		return fmt.Errorf("blueprint ID is required")
 	}
-	template, err := s.templateStore.GetByID(deployment.TemplateID)
+	blueprint, err := s.blueprintStore.GetByID(deployment.BlueprintID)
 	if err != nil {
-		return fmt.Errorf("failed to check template: %w", err)
+		return fmt.Errorf("failed to check blueprint: %w", err)
 	}
-	if template == nil {
-		return fmt.Errorf("template with ID %s not found", deployment.TemplateID)
+	if blueprint == nil {
+		return fmt.Errorf("blueprint with ID %s not found", deployment.BlueprintID)
 	}
 
 	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
 	}
 
-	// Get the template
-	template, err := s.templateStore.GetByID(deployment.TemplateID)
+	// Get the blueprint
+	blueprint, err := s.blueprintStore.GetByID(deployment.BlueprintID)
 	if err != nil {
-		return fmt.Errorf("failed to retrieve template: %w", err)
+		return fmt.Errorf("failed to retrieve blueprint: %w", err)
 	}
-	if template == nil {
-		return fmt.Errorf("template with ID %s not found", deployment.TemplateID)
+	if blueprint == nil {
+		return fmt.Errorf("blueprint with ID %s not found", deployment.BlueprintID)
 	}
 
-	// Use the template config to set up deployed apps
-	var templateConfig models.TemplateConfig
-	if err := json.Unmarshal([]byte(template.ConfigJSON), &templateConfig); err != nil {
-		return fmt.Errorf("failed to parse template config: %w", err)
+	// Use the blueprint config to set up deployed apps
+	var blueprintConfig models.BlueprintConfig
+	if err := json.Unmarshal([]byte(blueprint.ConfigJSON), &blueprintConfig); err != nil {
+		return fmt.Errorf("failed to parse blueprint config: %w", err)
 	}
 
-	// Create deployed apps for each app in the template
-	for _, appConfig := range templateConfig.Apps {
-		// Get the app
-		app, err := s.appStore.GetByID(appConfig.ID)
+	// Create deployed apps for each component in the blueprint
+	for _, componentConfig := range blueprintConfig.Components {
+		// Get the component
+		component, err := s.componentStore.GetByID(componentConfig.ID)
 		if err != nil {
-			return fmt.Errorf("failed to retrieve app: %w", err)
+			return fmt.Errorf("failed to retrieve component: %w", err)
 		}
-		if app == nil {
-			return fmt.Errorf("app with ID %s not found", appConfig.ID)
+		if component == nil {
+			return fmt.Errorf("component with ID %s not found", componentConfig.ID)
 		}
 
 		// Create a deployed app
-		deployedApp := models.DeployedApp{
+		deployedComponent := models.DeployedComponent{
 			ID:           uuid.New().String(),
 			DeploymentID: deployment.ID,
-			AppID:        app.ID,
+			ComponentID:  component.ID,
 			Status:       string(models.PENDING_APP),
-			Version:      app.Version,
+			Version:      component.Version,
 			URL:          "", // Will be set during deployment
-			PodCount:     appConfig.Autoscaling.MinReplicas,
+			PodCount:     componentConfig.Autoscaling.MinReplicas,
 			HealthStatus: string(models.HEALTHY),
 			Resources: models.ResourceAllocation{
-				CPU:     appConfig.Resources.CPU,
-				Memory:  appConfig.Resources.Memory,
-				Storage: appConfig.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
 }
 
 // processDeployment handles the actual deployment process
-func (s *DeploymentService) processDeployment(deploymentID string) {
-	// This would be an async process in a real system
-	// For now, we just update the status after a short delay to simulate the process
-
+func (s *DeploymentService) processDeployment(ctx context.Context, deployment *models.Deployment) {
 	// Update status to deploying
-	_ = s.UpdateDeploymentStatus(deploymentID, string(models.DEPLOYING))
+	_ = s.UpdateDeploymentStatus(deployment.ID, string(models.DEPLOYING))
+
+	s.logger.WithField("deployment_id", deployment.ID).Info("Starting deployment process")
 
-	// In a real system, this would be where you'd:
-	// 1. Provision infrastructure
-	// 2. Deploy containers/apps
-	// 3. Configure networking
-	// 4. Setup monitoring
-	// etc.
+	// Check if we have a valid instance
+	if deployment.InstanceID == "" {
+		s.logger.WithField("deployment_id", deployment.ID).Error("No instance ID provided for deployment")
+		_ = s.UpdateDeploymentStatus(deployment.ID, string(models.FAILED_DEPLOYMENT))
+		return
+	}
+	// Setup the deployed apps (database setup, migrations, etc.)
+	if err := s.setupDeployedComponents(deployment); err != nil {
+		s.logger.WithError(err).WithField("deployment_id", deployment.ID).Error("Failed to setup deployed apps")
+		_ = s.UpdateDeploymentStatus(deployment.ID, string(models.FAILED_DEPLOYMENT))
+		return
+	}
 
-	// For this demo, we'll just update the status after a short delay
-	time.Sleep(2 * time.Second)
+	// Update the deployment with completed status
+	deployment.Status = string(models.DEPLOYED)
+	deployment.LastDeployedAt = time.Now()
 
-	// Update status to deployed or failed (randomly for demonstration)
-	if time.Now().Unix()%2 == 0 { // Random success/failure
-		_ = s.UpdateDeploymentStatus(deploymentID, string(models.DEPLOYED))
-	} else {
-		_ = s.UpdateDeploymentStatus(deploymentID, string(models.FAILED_DEPLOYMENT))
+	if err := s.store.Update(deployment); err != nil {
+		s.logger.WithError(err).WithField("deployment_id", deployment.ID).Error("Failed to update deployment status")
 	}
+
+	s.logger.WithField("deployment_id", deployment.ID).Info("Deployment completed successfully")
 }
 
 // processDeploymentCleanup handles the cleanup process for deleted deployments
 func (s *DeploymentService) processDeploymentCleanup(deploymentID string) {
-	// This would be an async process in a real system
-	// In a real system, this would:
-	// 1. Deprovision infrastructure
-	// 2. Clean up resources
-	// 3. Remove configuration
+	// Get the deployment
+	deployment, err := s.store.GetByID(deploymentID)
+	if err != nil {
+		s.logger.WithError(err).WithField("deployment_id", deploymentID).Error("Failed to get deployment for cleanup")
+		return
+	}
 
-	// For this demo, we'll just delete after a short delay
-	time.Sleep(2 * time.Second)
+	if deployment == nil {
+		s.logger.WithField("deployment_id", deploymentID).Error("Deployment not found for cleanup")
+		return
+	}
+
+	s.logger.WithField("deployment_id", deploymentID).Info("Starting deployment cleanup process")
+
+	ctx := context.Background()
+
+	// Otherwise, deprovision the instance
+	provider, ok := cloud.GetProvider(deployment.Provider)
+	if !ok {
+		s.logger.WithField("provider", deployment.Provider).Error("Provider not found for cleanup")
+	} else {
+		s.logger.WithField("instance_id", deployment.InstanceID).Info("Terminating instance")
+
+		if err := provider.ResetInstance(ctx, deployment.InstanceID); err != nil {
+			s.logger.WithError(err).WithField("instance_id", deployment.InstanceID).
+				Error("Failed to terminate instance")
+		}
+	}
 
 	// 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

+ 0 - 153
services/templates.go

@@ -1,153 +0,0 @@
-package services
-
-import (
-	"fmt"
-
-	"git.linuxforward.com/byop/byop-engine/dbstore"
-	"git.linuxforward.com/byop/byop-engine/models"
-	"github.com/google/uuid"
-)
-
-// TemplateService handles business logic for templates
-type TemplateService struct {
-	store *dbstore.TemplateStore
-}
-
-// NewTemplateService creates a new TemplateService
-func NewTemplateService(store *dbstore.TemplateStore) *TemplateService {
-	return &TemplateService{store: store}
-}
-
-// CreateTemplate creates a new deployment template
-func (s *TemplateService) CreateTemplate(template *models.Template) error {
-	// Generate UUID if not provided
-	if template.ID == "" {
-		template.ID = uuid.New().String()
-	}
-
-	// Validate template configuration
-	if err := validateTemplateConfig(template.Config); err != nil {
-		return fmt.Errorf("invalid template configuration: %w", err)
-	}
-
-	// Persist the template
-	return s.store.Create(template)
-}
-
-// GetTemplate retrieves a template by ID
-func (s *TemplateService) GetTemplate(id string) (*models.Template, error) {
-	template, err := s.store.GetByID(id)
-	if err != nil {
-		return nil, fmt.Errorf("failed to retrieve template: %w", err)
-	}
-	return template, nil
-}
-
-// UpdateTemplate updates an existing template
-func (s *TemplateService) UpdateTemplate(template *models.Template) error {
-	if template.ID == "" {
-		return fmt.Errorf("template ID is required for update")
-	}
-
-	// Check if template exists
-	existingTemplate, err := s.store.GetByID(template.ID)
-	if err != nil {
-		return fmt.Errorf("failed to check if template exists: %w", err)
-	}
-	if existingTemplate == nil {
-		return fmt.Errorf("template with ID %s not found", template.ID)
-	}
-
-	// Validate template configuration
-	if err := validateTemplateConfig(template.Config); err != nil {
-		return fmt.Errorf("invalid template configuration: %w", err)
-	}
-
-	return s.store.Update(template)
-}
-
-// DeleteTemplate deletes a template by ID
-func (s *TemplateService) DeleteTemplate(id string) error {
-	// Check if template exists
-	template, err := s.store.GetByID(id)
-	if err != nil {
-		return fmt.Errorf("failed to check if template exists: %w", err)
-	}
-	if template == nil {
-		return fmt.Errorf("template with ID %s not found", id)
-	}
-
-	// Check if the template has deployments
-	templateWithDeployments, err := s.store.GetTemplateWithDeployments(id)
-	if err != nil {
-		return fmt.Errorf("failed to check template deployments: %w", err)
-	}
-
-	// Don't allow deletion if there are active deployments
-	if len(templateWithDeployments.Deployments) > 0 {
-		return fmt.Errorf("cannot delete template with active deployments")
-	}
-
-	return s.store.Delete(id)
-}
-
-// ListTemplates retrieves all templates with optional filtering
-func (s *TemplateService) ListTemplates(filter map[string]interface{}) ([]*models.Template, error) {
-	return s.store.List(filter)
-}
-
-// GetTemplateDeployments retrieves all deployments for a template
-func (s *TemplateService) GetTemplateDeployments(id string) ([]models.Deployment, error) {
-	// First check if the template exists
-	template, err := s.store.GetByID(id)
-	if err != nil {
-		return nil, fmt.Errorf("failed to check if template exists: %w", err)
-	}
-	if template == nil {
-		return nil, fmt.Errorf("template with ID %s not found", id)
-	}
-
-	// Get template with deployments
-	templateWithDeployments, err := s.store.GetTemplateWithDeployments(id)
-	if err != nil {
-		return nil, fmt.Errorf("failed to retrieve template deployments: %w", err)
-	}
-
-	return templateWithDeployments.Deployments, nil
-}
-
-// GetTemplateByVersion retrieves a template by name and version
-func (s *TemplateService) GetTemplateByVersion(name, version string) (*models.Template, error) {
-	template, err := s.store.GetByVersion(name, version)
-	if err != nil {
-		return nil, fmt.Errorf("failed to retrieve template: %w", err)
-	}
-	return template, nil
-}
-
-// validateTemplateConfig validates the template configuration
-func validateTemplateConfig(config models.TemplateConfig) error {
-	// Validate that at least one app is defined
-	if len(config.Apps) == 0 {
-		return fmt.Errorf("template must define at least one app")
-	}
-
-	// Validate each app in the template
-	for i, app := range config.Apps {
-		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
-}