390 lines
11 KiB
Go
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
|
|
}
|