deb-bootc-compose/internal/ostree/ostree.go
2025-08-18 23:32:51 -07:00

390 lines
11 KiB
Go

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
}