123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- package services
- import (
- "context"
- "fmt"
- "os"
- "strings"
- "git.linuxforward.com/byop/byop-engine/dbstore"
- "git.linuxforward.com/byop/byop-engine/models"
- git "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/sirupsen/logrus"
- )
- // AppImporter handles the import of applications from docker-compose files
- type AppImporter struct {
- store *dbstore.SQLiteStore
- parser *ComposeParser
- builderSvc *Builder
- entry *logrus.Entry
- registryURL string
- }
- // NewAppImporter creates a new AppImporter instance
- func NewAppImporter(store *dbstore.SQLiteStore, builderSvc *Builder, registryURL string) *AppImporter {
- return &AppImporter{
- store: store,
- parser: NewComposeParser(),
- builderSvc: builderSvc,
- entry: logrus.WithField("service", "AppImporter"),
- registryURL: registryURL,
- }
- }
- // ReviewComposeFile reviews a docker-compose file from a git repository
- func (ai *AppImporter) ReviewComposeFile(ctx context.Context, req models.AppImportRequest) (*models.AppImportReview, error) {
- ai.entry.Infof("Reviewing compose file from %s (branch: %s, path: %s)", req.SourceURL, req.Branch, req.ComposePath)
- // Create temporary directory for cloning
- tempDir, err := os.MkdirTemp("", "byop-import-*")
- if err != nil {
- return nil, fmt.Errorf("failed to create temp directory: %w", err)
- }
- defer os.RemoveAll(tempDir)
- // Clone the repository
- if err := ai.cloneRepository(req.SourceURL, req.Branch, tempDir); err != nil {
- return &models.AppImportReview{
- Valid: false,
- Error: fmt.Sprintf("Failed to clone repository: %v", err),
- }, nil
- }
- // Set default compose path if not provided
- composePath := req.ComposePath
- if composePath == "" {
- composePath = "docker-compose.yml"
- }
- // Parse the compose file
- review, err := ai.parser.ParseComposeFile(tempDir, composePath)
- if err != nil {
- return nil, fmt.Errorf("failed to parse compose file: %w", err)
- }
- // Override app name if provided in request
- if req.AppName != "" {
- review.AppName = req.AppName
- }
- return review, nil
- }
- // CreateAppFromCompose creates an app and its components from a docker-compose import
- func (ai *AppImporter) CreateAppFromCompose(ctx context.Context, req models.AppImportCreateRequest, userID uint) (*models.App, error) {
- ai.entry.Infof("Creating app '%s' from compose import for user %d", req.ConfirmedAppName, userID)
- // First, review the compose file to get the services
- review, err := ai.ReviewComposeFile(ctx, req.AppImportRequest)
- if err != nil {
- return nil, fmt.Errorf("failed to review compose file: %w", err)
- }
- if !review.Valid {
- return nil, fmt.Errorf("invalid compose file: %s", review.Error)
- }
- // Create the app
- app := &models.App{
- UserID: userID,
- Name: req.ConfirmedAppName,
- Description: fmt.Sprintf("Imported from docker-compose.yml from %s", req.SourceURL),
- Status: models.AppStatusBuilding,
- Components: "[]", // Will be updated after creating components
- }
- if err := ai.store.CreateApp(ctx, app); err != nil {
- return nil, fmt.Errorf("failed to create app: %w", err)
- }
- ai.entry.Infof("Created app ID %d: %s", app.ID, app.Name)
- // Create components for each service
- var componentIDs []uint
- for _, service := range review.Services {
- component, err := ai.createComponentFromService(ctx, app.ID, userID, service, req)
- if err != nil {
- ai.entry.Errorf("Failed to create component for service %s: %v", service.Name, err)
- // Continue with other services, don't fail the entire import
- continue
- }
- componentIDs = append(componentIDs, component.ID)
- }
- // Update app with component IDs
- if err := ai.updateAppComponents(ctx, app.ID, componentIDs); err != nil {
- ai.entry.Errorf("Failed to update app components: %v", err)
- // Non-fatal error, the app and components are still created
- }
- // Update app status
- if len(componentIDs) > 0 {
- app.Status = models.AppStatusBuilding
- } else {
- app.Status = models.AppStatusFailed
- app.ErrorMsg = "No components could be created from the compose file"
- }
- if err := ai.store.UpdateAppStatus(ctx, app.ID, app.Status, app.ErrorMsg); err != nil {
- ai.entry.Errorf("Failed to update app status: %v", err)
- }
- ai.entry.Infof("Successfully created app %s with %d components", app.Name, len(componentIDs))
- return app, nil
- }
- // createComponentFromService creates a component from a compose service
- func (ai *AppImporter) createComponentFromService(ctx context.Context, appID, userID uint, service models.ComposeService, req models.AppImportCreateRequest) (*models.Component, error) {
- ai.entry.Infof("Creating component for service: %s (source: %s)", service.Name, service.Source)
- // Determine component type based on service configuration
- componentType := "web"
- if len(service.Ports) == 0 {
- componentType = "worker"
- }
- // Resolve build context for docker-compose imports
- buildContext := service.BuildContext
- if buildContext == "" {
- buildContext = "."
- }
- // Clean up the build context path to remove ./ prefix and ensure it's relative
- buildContext = strings.TrimPrefix(buildContext, "./")
- // Create component
- component := &models.Component{
- UserID: userID,
- Name: service.Name,
- Description: fmt.Sprintf("Service from docker-compose: %s", service.Name),
- Type: componentType,
- Status: "pending",
- Repository: req.SourceURL,
- Branch: req.Branch,
- SourceType: "docker-compose",
- ServiceName: service.Name,
- BuildContext: buildContext,
- DockerfilePath: service.Dockerfile,
- }
- if err := ai.store.CreateComponent(ctx, component); err != nil {
- return nil, fmt.Errorf("failed to create component: %w", err)
- }
- ai.entry.Infof("Created component ID %d: %s", component.ID, component.Name)
- // Handle the component based on its source type
- switch service.Source {
- case "build":
- // Queue a build job for this component
- if err := ai.queueBuildJob(ctx, component, service, req); err != nil {
- ai.entry.Errorf("Failed to queue build job for component %d: %v", component.ID, err)
- ai.store.UpdateComponentStatus(ctx, component.ID, "failed", fmt.Sprintf("Failed to queue build: %v", err))
- }
- case "image":
- // Mark component as ready and set image info
- ai.store.UpdateComponentStatus(ctx, component.ID, "ready", "")
- ai.store.UpdateComponentImageInfo(ctx, component.ID, "latest", service.Image)
- ai.entry.Infof("Component %d marked as ready with image: %s", component.ID, service.Image)
- default:
- ai.entry.Warnf("Unknown service source type: %s", service.Source)
- ai.store.UpdateComponentStatus(ctx, component.ID, "failed", "Unknown service source type")
- }
- return component, nil
- }
- // queueBuildJob queues a build job for a component that needs to be built
- func (ai *AppImporter) queueBuildJob(ctx context.Context, component *models.Component, service models.ComposeService, req models.AppImportCreateRequest) error {
- // Use the build context already stored in the component
- buildContext := component.BuildContext
- if buildContext == "" {
- buildContext = "."
- }
- ai.entry.Infof("Using build context for service %s: %s", service.Name, buildContext)
- // Prepare build request
- buildReq := models.BuildRequest{
- ComponentID: component.ID,
- SourceURL: req.SourceURL,
- Version: req.Branch,
- ImageName: fmt.Sprintf("byop-component-%d", component.ID),
- RegistryURL: ai.registryURL,
- BuildContext: buildContext,
- Dockerfile: component.DockerfilePath,
- Source: "docker-compose",
- }
- // Set default dockerfile if not specified
- if buildReq.Dockerfile == "" {
- buildReq.Dockerfile = "Dockerfile"
- }
- _, err := ai.builderSvc.QueueBuildJob(ctx, buildReq)
- if err != nil {
- return fmt.Errorf("failed to queue build job: %w", err)
- }
- ai.entry.Infof("Queued build job for component %d (service: %s)", component.ID, service.Name)
- return nil
- }
- // updateAppComponents updates the app's components field with the component IDs
- func (ai *AppImporter) updateAppComponents(ctx context.Context, appID uint, componentIDs []uint) error {
- // Convert component IDs to JSON string
- componentsJSON := "["
- for i, id := range componentIDs {
- if i > 0 {
- componentsJSON += ","
- }
- componentsJSON += fmt.Sprintf("%d", id)
- }
- componentsJSON += "]"
- // Update the app in the database
- app, err := ai.store.GetAppByID(ctx, appID)
- if err != nil {
- return fmt.Errorf("failed to get app: %w", err)
- }
- app.Components = componentsJSON
- if err := ai.store.UpdateApp(ctx, app); err != nil {
- return fmt.Errorf("failed to update app components: %w", err)
- }
- return nil
- }
- // cloneRepository clones a Git repository to the specified directory
- func (ai *AppImporter) cloneRepository(repoURL, branch, targetDir string) error {
- ai.entry.Infof("Cloning repository %s (branch: %s) to %s", repoURL, branch, targetDir)
- // Clone options
- cloneOptions := &git.CloneOptions{
- URL: repoURL,
- Progress: nil, // We could add progress tracking here
- }
- // Set branch if specified
- if branch != "" {
- cloneOptions.ReferenceName = plumbing.NewBranchReferenceName(branch)
- cloneOptions.SingleBranch = true
- }
- // Clone the repository
- _, err := git.PlainClone(targetDir, false, cloneOptions)
- if err != nil {
- return fmt.Errorf("failed to clone repository: %w", err)
- }
- ai.entry.Infof("Successfully cloned repository to %s", targetDir)
- return nil
- }
|