Skip to content

Commit

Permalink
feat: add GC
Browse files Browse the repository at this point in the history
  • Loading branch information
nkraetzschmar committed Nov 11, 2024
1 parent b193633 commit 23e513f
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 40 deletions.
2 changes: 1 addition & 1 deletion prepare_source
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import_src src
pkg=gardenlinux-update
version=0.1
version=0.2
2 changes: 1 addition & 1 deletion src/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
gardenlinux-update:
gardenlinux-update: main.go
GO111MODULE=on go build -mod=vendor

install:
Expand Down
221 changes: 183 additions & 38 deletions src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,31 @@ package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
"syscall"

"oras.land/oras-go/v2/registry/remote"
)

func getAvailableSpace(path string) (uint64, error) {
var stat syscall.Statfs_t

err := syscall.Statfs(path, &stat)
if err != nil {
return 0, err
}

available := stat.Bavail * uint64(stat.Bsize)
return available, nil
}

func parseOsRelease(data string) map[string]string {
result := make(map[string]string)
lines := strings.Split(data, "\n")
Expand All @@ -27,17 +43,17 @@ func parseOsRelease(data string) map[string]string {
return result
}

func getCname(path string) (string, error) {
func getCname(path string) (string, string, error) {
os_release_content, err := os.ReadFile(path)
if err != nil {
return "", err
return "", "", err
}

os_release := parseOsRelease(string(os_release_content))
version := os_release["GARDENLINUX_VERSION"]
cname := strings.Trim(os_release["GARDENLINUX_CNAME"], "-"+version)

return cname, nil
return cname, version, nil
}

func getManifest(repo *remote.Repository, ctx context.Context, ref string) (map[string]interface{}, error) {
Expand Down Expand Up @@ -90,79 +106,161 @@ func getManifestDigestByCname(repo *remote.Repository, ctx context.Context, tag
return digest, nil
}

func getLayerByMediaType(repo *remote.Repository, ctx context.Context, digest string, media_type string) (string, error) {
func getLayerByMediaType(repo *remote.Repository, ctx context.Context, digest string, media_type string) (string, uint64, error) {
manifest, err := getManifest(repo, ctx, digest)
if err != nil {
return "", err
return "", 0, err
}

var layer string
var size uint64

for _, entry := range manifest["layers"].([]interface{}) {
item := entry.(map[string]interface{})
item_digest := item["digest"].(string)
item_size := uint64(item["size"].(float64))
item_media_type := item["mediaType"].(string)

if item_media_type == media_type {
layer = item_digest
size = item_size
break
}
}

return layer, nil
return layer, size, nil
}

func downloadArtifact(target_path string, repo_url string, version string, cname string, media_type string) error {
repo, err := remote.NewRepository(repo_url)
func getFilesWithPrefix(dir string, prefix string) ([]string, error) {
var files []string

entries, err := os.ReadDir(dir)
if err != nil {
return err
return nil, err
}

ctx := context.Background()

digest, err := getManifestDigestByCname(repo, ctx, version, cname)
if err != nil {
return err
for _, entry := range entries {
if entry.Type().IsRegular() && strings.HasPrefix(entry.Name(), prefix) {
files = append(files, entry.Name())
}
}

layer, err := getLayerByMediaType(repo, ctx, digest, media_type)
if err != nil {
return err
return files, nil
}

type FileInfo struct {
Filename string
Version string
BootBlessed bool
TriesRemaining int
}

func parseFileInfos(filenames []string, prefix string) []FileInfo {
var result []FileInfo
for _, filename := range filenames {
name := strings.TrimSuffix(strings.TrimPrefix(filename, prefix+"-"), ".efi")
parts := strings.Split(name, "+")
version := parts[0]

boot_blessed := true
tries_remaining := 0

if len(parts) > 1 {
boot_blessed = false
counting_part := strings.Split(parts[1], "-")[0]
tries_remaining, _ = strconv.Atoi(counting_part)
}

result = append(result, FileInfo{
Filename: filename,
Version: version,
BootBlessed: boot_blessed,
TriesRemaining: tries_remaining,
})
}
return result
}

layer_descriptor, err := repo.Blobs().Resolve(ctx, layer)
if err != nil {
return err
func compareVersions(v1 string, v2 string) int {
v1Parts := strings.Split(v1, ".")
v2Parts := strings.Split(v2, ".")
for i := 0; i < len(v1Parts) && i < len(v2Parts); i++ {
v1Int, _ := strconv.Atoi(v1Parts[i])
v2Int, _ := strconv.Atoi(v2Parts[i])
if v1Int != v2Int {
if v1Int > v2Int {
return 1
}
return -1
}
}
if len(v1Parts) > len(v2Parts) {
return 1
} else if len(v1Parts) < len(v2Parts) {
return -1
}
return 0
}

fmt.Printf("downloading %s@%s -> %s\n", repo_url, layer_descriptor.Digest, target_path)
func sortFileInfos(fileInfos []FileInfo) {
sort.Slice(fileInfos, func(i, j int) bool {
if fileInfos[i].BootBlessed != fileInfos[j].BootBlessed {
return !fileInfos[i].BootBlessed
}
if fileInfos[i].TriesRemaining != fileInfos[j].TriesRemaining {
return fileInfos[i].TriesRemaining < fileInfos[j].TriesRemaining
}
return compareVersions(fileInfos[i].Version, fileInfos[j].Version) < 0
})
}

layer_stream, err := repo.Fetch(ctx, layer_descriptor)
func garbageClean(directory, cname, current_version string, size_wanted int64) error {
files, err := getFilesWithPrefix(directory, cname)
if err != nil {
return err
}
defer layer_stream.Close()

target_file, err := os.Create(target_path)
if err != nil {
return err
file_infos := parseFileInfos(files, cname)
sortFileInfos(file_infos)

for _, file_info := range file_infos {
if file_info.Version == current_version {
continue
}

file_path := directory + "/" + file_info.Filename
file_stat, err := os.Stat(file_path)
if err != nil {
return err
}

file_size := file_stat.Size()

err = os.Remove(file_path)
if err != nil {
return err
}

fmt.Printf("cleaned up %s\n", file_path)

size_wanted -= file_size
if size_wanted <= 0 {
break
}
}
defer target_file.Close()

_, err = io.Copy(target_file, layer_stream)
if err != nil {
panic(err)
if size_wanted > 0 {
return errors.New("garbage clean could not free enough space")
}

return nil
}

func main() {
repo := flag.String("repo", "ghcr.io/gardenlinux/gl-oci", "OCI repository to download from")
repo_url := flag.String("repo", "ghcr.io/gardenlinux/gl-oci", "OCI repository to download from")
media_type := flag.String("media-type", "application/io.gardenlinux.uki", "artifact media type to fetch")
target_dir := flag.String("target-dir", "/efi/EFI/Linux", "directory to write artifacts to")
os_release_path := flag.String("os-release", "/etc/os-release", "alternative path where the os-release file is read from")
override_cname := flag.String("cname", "", "override cname, by default the correct cname is determined automatically from /etc/os-release")

flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] <version>\n", os.Args[0])
Expand All @@ -176,19 +274,66 @@ func main() {
}
version := flag.Arg(0)

var cname string
if *override_cname == "" {
current_cname, err := getCname(*os_release_path)
cname, current_version, err := getCname(*os_release_path)
if err != nil {
panic(err)
}

ctx := context.Background()

repo, err := remote.NewRepository(*repo_url)
if err != nil {
panic(err)
}

digest, err := getManifestDigestByCname(repo, ctx, version, cname)
if err != nil {
panic(err)
}

layer, size, err := getLayerByMediaType(repo, ctx, digest, *media_type)
if err != nil {
panic(err)
}

space_required := size + (1024 * 1024)

target_path := *target_dir + "/" + cname + "-" + version + "+3.efi"

space, err := getAvailableSpace(*target_dir)
if err != nil {
panic(err)
}

if space < space_required {
space_wanted := space_required - space
err := garbageClean(*target_dir, cname, current_version, int64(space_wanted))
if err != nil {
panic(err)
}
}

fmt.Printf("downloading %s@%s -> %s\n", *repo_url, layer, target_path)

cname = current_cname
layer_descriptor, err := repo.Blobs().Resolve(ctx, layer)
if err != nil {
panic(err)
}

target_path := *target_dir + "/" + cname + "-" + version + "+3.efi"
layer_stream, err := repo.Fetch(ctx, layer_descriptor)
if err != nil {
panic(err)
}
defer layer_stream.Close()

if err := downloadArtifact(target_path, *repo, version, cname, *media_type); err != nil {
target_file, err := os.Create(target_path)
if err != nil {
panic(err)
}
defer target_file.Close()

_, err = io.Copy(target_file, layer_stream)
if err != nil {
panic(err)
}
}

0 comments on commit 23e513f

Please sign in to comment.