package ostree import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strings" "time" "github.com/sirupsen/logrus" ) // Tool represents the OSTree integration tool interface type Tool interface { ComposeTree() (*Commit, error) ValidateTreefile() error GetRepositoryInfo() (map[string]interface{}, error) CreateContainer(name, path string) error } // Config represents OSTree configuration type Config struct { RepoPath string `json:"repo_path"` TreefilePath string `json:"treefile_path"` LogDir string `json:"log_dir"` Version string `json:"version"` UpdateSummary bool `json:"update_summary"` ForceNewCommit bool `json:"force_new_commit"` UnifiedCore bool `json:"unified_core"` ExtraConfig map[string]interface{} `json:"extra_config"` OSTreeRef string `json:"ostree_ref"` RootDir string `json:"root_dir"` WorkDir string `json:"work_dir"` CacheDir string `json:"cache_dir"` ContainerOutput bool `json:"container_output"` } // Commit represents an OSTree commit type Commit struct { ID string `json:"id"` Ref string `json:"ref"` Timestamp string `json:"timestamp"` Version string `json:"version"` Metadata map[string]interface{} `json:"metadata"` } // FullOSTreeTool represents the comprehensive OSTree integration tool type FullOSTreeTool struct { logger *logrus.Logger config *Config } // NewTool creates a new OSTree tool (interface compatibility) func NewTool(config *Config) (Tool, error) { return NewFullOSTreeTool(config), nil } // NewFullOSTreeTool creates a new comprehensive OSTree tool func NewFullOSTreeTool(config *Config) *FullOSTreeTool { return &FullOSTreeTool{ logger: logrus.New(), config: config, } } // ComposeTree composes an OSTree tree from a treefile using apt-ostree func (t *FullOSTreeTool) ComposeTree() (*Commit, error) { t.logger.Info("Starting OSTree tree composition using apt-ostree") // Ensure repository directory exists if err := os.MkdirAll(t.config.RepoPath, 0755); err != nil { return nil, fmt.Errorf("failed to create repository directory: %w", err) } // Ensure log directory exists if err := os.MkdirAll(t.config.LogDir, 0755); err != nil { return nil, fmt.Errorf("failed to create log directory: %w", err) } // Ensure work directory exists if t.config.WorkDir != "" { if err := os.MkdirAll(t.config.WorkDir, 0755); err != nil { return nil, fmt.Errorf("failed to create work directory: %w", err) } } // Ensure cache directory exists if t.config.CacheDir != "" { if err := os.MkdirAll(t.config.CacheDir, 0755); err != nil { return nil, fmt.Errorf("failed to create cache directory: %w", err) } } // Create commit ID file path commitIDFile := filepath.Join(t.config.LogDir, "commitid") // Build apt-ostree compose tree command cmd := t.buildComposeCommand(commitIDFile) // Execute the command t.logger.Infof("Executing: %s", strings.Join(cmd.Args, " ")) if err := cmd.Run(); err != nil { return nil, fmt.Errorf("failed to compose tree with apt-ostree: %w", err) } // Read commit ID commitID, err := t.readCommitID(commitIDFile) if err != nil { return nil, fmt.Errorf("failed to read commit ID: %w", err) } // Get ref from treefile ref, err := t.getRefFromTreefile() if err != nil { return nil, fmt.Errorf("failed to get ref from treefile: %w", err) } // Update ref in repository if err := t.updateRef(ref, commitID); err != nil { return nil, fmt.Errorf("failed to update ref: %w", err) } // Update summary if requested if t.config.UpdateSummary { if err := t.updateSummary(); err != nil { t.logger.Warnf("Failed to update summary: %v", err) } } // Create commit object commit := &Commit{ ID: commitID, Ref: ref, Timestamp: time.Now().Format(time.RFC3339), Version: t.config.Version, Metadata: map[string]interface{}{ "version": t.config.Version, "composed_at": time.Now().Format(time.RFC3339), "tool": "apt-ostree", }, } t.logger.Infof("OSTree tree composition completed: %s", commitID) return commit, nil } // buildComposeCommand builds the apt-ostree compose tree command func (t *FullOSTreeTool) buildComposeCommand(commitIDFile string) *exec.Cmd { // Use apt-ostree compose tree for proper Debian integration args := []string{ "apt-ostree", "compose", "tree", } // Add repository path if t.config.RepoPath != "" { args = append(args, "-r", t.config.RepoPath) } // Add work directory if t.config.WorkDir != "" { args = append(args, "--workdir", t.config.WorkDir) } // Add cache directory if t.config.CacheDir != "" { args = append(args, "--cachedir", t.config.CacheDir) } // Add force new commit if requested if t.config.ForceNewCommit { args = append(args, "--force-nocache") } // Add container output if requested if t.config.ContainerOutput { args = append(args, "--container") } // Add metadata if t.config.Version != "" { args = append(args, "--add-metadata-string", fmt.Sprintf("version=%s", t.config.Version)) } // Add commit ID output args = append(args, "--write-commitid-to", commitIDFile) // Add treefile path (required argument) args = append(args, t.config.TreefilePath) // Create command cmd := exec.Command(args[0], args[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd } // ensureRepoInitialized initializes the ostree repository if needed func (t *FullOSTreeTool) ensureRepoInitialized() error { repoConfig := filepath.Join(t.config.RepoPath, "config") if _, err := os.Stat(repoConfig); err == nil { return nil } if err := os.MkdirAll(t.config.RepoPath, 0755); err != nil { return err } cmd := exec.Command("ostree", "init", fmt.Sprintf("--repo=%s", t.config.RepoPath), "--mode=archive") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } // readCommitID reads the commit ID from the commit ID file func (t *FullOSTreeTool) readCommitID(commitIDFile string) (string, error) { data, err := os.ReadFile(commitIDFile) if err != nil { return "", err } return strings.TrimSpace(string(data)), nil } // getRefFromTreefile extracts the ref from the treefile func (t *FullOSTreeTool) getRefFromTreefile() (string, error) { // For now, we'll implement a simple ref extraction // In the future, this could parse the treefile more comprehensively data, err := os.ReadFile(t.config.TreefilePath) if err != nil { return "", err } // Simple regex-like extraction - look for "ref": "value" content := string(data) if strings.Contains(content, `"ref"`) { // Extract ref value - this is a simplified approach // In production, use proper JSON parsing lines := strings.Split(content, "\n") for _, line := range lines { if strings.Contains(line, `"ref"`) { parts := strings.Split(line, `"ref"`) if len(parts) > 1 { refPart := parts[1] if strings.Contains(refPart, `"`) { refParts := strings.Split(refPart, `"`) if len(refParts) > 2 { return refParts[1], nil } } } } } } // Default ref if none found return "debian/bootc", nil } // updateRef updates the ref in the OSTree repository func (t *FullOSTreeTool) updateRef(ref, commitID string) error { t.logger.Infof("Updating ref %s to commit %s", ref, commitID) // Create refs/heads directory headsDir := filepath.Join(t.config.RepoPath, "refs", "heads") if err := os.MkdirAll(headsDir, 0755); err != nil { return fmt.Errorf("failed to create refs/heads directory: %w", err) } // Write ref file refPath := filepath.Join(headsDir, ref) if err := os.WriteFile(refPath, []byte(commitID+"\n"), 0644); err != nil { return fmt.Errorf("failed to write ref file: %w", err) } t.logger.Infof("Ref %s updated successfully", ref) return nil } // updateSummary updates the OSTree repository summary func (t *FullOSTreeTool) updateSummary() error { t.logger.Info("Updating OSTree repository summary") cmd := exec.Command("ostree", "summary", "-u", fmt.Sprintf("--repo=%s", t.config.RepoPath)) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("failed to update summary: %w", err) } t.logger.Info("Summary updated successfully") return nil } // CreateContainer creates an OSTree native container using apt-ostree func (t *FullOSTreeTool) CreateContainer(name, path string) error { t.logger.Infof("Creating container %s using apt-ostree", name) // Use apt-ostree compose image to generate container args := []string{ "apt-ostree", "compose", "image", } // Add treefile if available (first argument) if t.config.TreefilePath != "" { args = append(args, t.config.TreefilePath) } // Add output path (second argument) args = append(args, path) // Create command cmd := exec.Command(args[0], args[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr t.logger.Infof("Executing: %s", strings.Join(cmd.Args, " ")) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to create container with apt-ostree: %w", err) } t.logger.Infof("Container %s created successfully", name) return nil } // GetRepositoryInfo returns information about the OSTree repository func (t *FullOSTreeTool) GetRepositoryInfo() (map[string]interface{}, error) { repo := map[string]interface{}{ "path": t.config.RepoPath, "refs": []string{}, "commits": []string{}, "summary": false, } // Check if repository exists if _, err := os.Stat(t.config.RepoPath); os.IsNotExist(err) { return repo, nil } // Get refs refsDir := filepath.Join(t.config.RepoPath, "refs", "heads") if refs, err := os.ReadDir(refsDir); err == nil { for _, ref := range refs { if !ref.IsDir() { repo["refs"] = append(repo["refs"].([]string), ref.Name()) } } } // Check for summary summaryFile := filepath.Join(t.config.RepoPath, "summary") if _, err := os.Stat(summaryFile); err == nil { repo["summary"] = true } return repo, nil } // ValidateTreefile validates the treefile for OSTree composition func (t *FullOSTreeTool) ValidateTreefile() error { t.logger.Info("Validating treefile") // Check if treefile exists if _, err := os.Stat(t.config.TreefilePath); os.IsNotExist(err) { return fmt.Errorf("treefile does not exist: %s", t.config.TreefilePath) } // Try to parse as JSON to validate basic structure data, err := os.ReadFile(t.config.TreefilePath) if err != nil { return fmt.Errorf("failed to read treefile: %w", err) } var treefile map[string]interface{} if err := json.Unmarshal(data, &treefile); err != nil { return fmt.Errorf("treefile is not valid JSON: %w", err) } // Check for required fields if _, ok := treefile["ref"]; !ok { t.logger.Warn("Treefile missing 'ref' field") } t.logger.Info("Treefile validation completed") return nil }