Add a WORKDIR instruction to the containerfile
{
if workdir != "" {
_, err := containerfile.WriteString(
fmt.Sprintf("WORKDIR %s\n", workdir),
)
if err != nil {
return err
}
}
return nil
}
Add a WORKDIR instruction to reset to the root directory
{
if workdir != "" {
_, err := containerfile.WriteString(
fmt.Sprintf("WORKDIR %s\n", "/"),
)
if err != nil {
return err
}
}
return nil
}
Load and build a Containerfile from the specified recipe
{
// load the recipe
recipe, err := LoadRecipe(recipePath)
if err != nil {
return api.Recipe{}, err
}
fmt.Printf("Building recipe %s\n", recipe.Name)
// build the Containerfile
err = BuildContainerfile(recipe)
if err != nil {
return api.Recipe{}, err
}
modules := 0
for _, stage := range recipe.Stages {
modules += len(stage.Modules)
}
fmt.Printf("Recipe %s built successfully\n", recipe.Name)
fmt.Printf("Processed %d stages\n", len(recipe.Stages))
fmt.Printf("Processed %d modules\n", modules)
return *recipe, nil
}
Generate a Containerfile from the recipe
{
containerfile, err := os.Create(recipe.Containerfile)
if err != nil {
return err
}
defer containerfile.Close()
for _, stage := range recipe.Stages {
// build the modules*
// * actually just build the commands that will be used
// in the Containerfile to build the modules
cmds, err := BuildModules(recipe, stage.Modules)
if err != nil {
return err
}
// FROM
if stage.Id != "" {
_, err = containerfile.WriteString(
fmt.Sprintf("# Stage: %s\n", stage.Id),
)
if err != nil {
return err
}
_, err = containerfile.WriteString(
fmt.Sprintf("FROM %s AS %s\n", stage.Base, stage.Id),
)
if err != nil {
return err
}
} else {
_, err = containerfile.WriteString(
fmt.Sprintf("FROM %s\n", stage.Base),
)
if err != nil {
return err
}
}
// COPY
if len(stage.Copy) > 0 {
for _, copy := range stage.Copy {
if len(copy.SrcDst) > 0 {
err = ChangeWorkingDirectory(copy.Workdir, containerfile)
if err != nil {
return err
}
for src, dst := range copy.SrcDst {
if copy.From != "" {
_, err = containerfile.WriteString(
fmt.Sprintf("COPY --from=%s %s %s\n", copy.From, src, dst),
)
if err != nil {
return err
}
} else {
_, err = containerfile.WriteString(
fmt.Sprintf("COPY %s %s\n", src, dst),
)
if err != nil {
return err
}
}
}
err = RestoreWorkingDirectory(copy.Workdir, containerfile)
if err != nil {
return err
}
}
}
}
// LABELS
for key, value := range stage.Labels {
_, err = containerfile.WriteString(
fmt.Sprintf("LABEL %s='%s'\n", key, value),
)
if err != nil {
return err
}
}
// ENV
for key, value := range stage.Env {
_, err = containerfile.WriteString(
fmt.Sprintf("ENV %s=%s\n", key, value),
)
if err != nil {
return err
}
}
// ARGS
for key, value := range stage.Args {
_, err = containerfile.WriteString(
fmt.Sprintf("ARG %s=%s\n", key, value),
)
if err != nil {
return err
}
}
// RUN(S)
if len(stage.Runs.Commands) > 0 {
err = ChangeWorkingDirectory(stage.Runs.Workdir, containerfile)
if err != nil {
return err
}
for _, cmd := range stage.Runs.Commands {
_, err = containerfile.WriteString(
fmt.Sprintf("RUN %s\n", cmd),
)
if err != nil {
return err
}
}
err = RestoreWorkingDirectory(stage.Runs.Workdir, containerfile)
if err != nil {
return err
}
}
// EXPOSE
for key, value := range stage.Expose {
_, err = containerfile.WriteString(
fmt.Sprintf("EXPOSE %s/%s\n", key, value),
)
if err != nil {
return err
}
}
// ADDS
if len(stage.Adds) > 0 {
for _, add := range stage.Adds {
if len(add.SrcDst) > 0 {
err = ChangeWorkingDirectory(add.Workdir, containerfile)
if err != nil {
return err
}
for src, dst := range add.SrcDst {
_, err = containerfile.WriteString(
fmt.Sprintf("ADD %s %s\n", src, dst),
)
if err != nil {
return err
}
}
}
err = RestoreWorkingDirectory(add.Workdir, containerfile)
if err != nil {
return err
}
}
}
// INCLUDES.CONTAINER
_, err = containerfile.WriteString(fmt.Sprintf("ADD %s /\n", recipe.IncludesPath))
if err != nil {
return err
}
for _, cmd := range cmds {
err = ChangeWorkingDirectory(cmd.Workdir, containerfile)
if err != nil {
return err
}
_, err = containerfile.WriteString(strings.Join(cmd.Command, "\n"))
if err != nil {
return err
}
err = RestoreWorkingDirectory(cmd.Workdir, containerfile)
if err != nil {
return err
}
}
// CMD
err = ChangeWorkingDirectory(stage.Cmd.Workdir, containerfile)
if err != nil {
return err
}
if len(stage.Cmd.Exec) > 0 {
_, err = containerfile.WriteString(
fmt.Sprintf("CMD [\"%s\"]\n", strings.Join(stage.Cmd.Exec, "\",\"")),
)
if err != nil {
return err
}
err = RestoreWorkingDirectory(stage.Cmd.Workdir, containerfile)
if err != nil {
return err
}
}
// ENTRYPOINT
err = ChangeWorkingDirectory(stage.Entrypoint.Workdir, containerfile)
if err != nil {
return err
}
if len(stage.Entrypoint.Exec) > 0 {
_, err = containerfile.WriteString(
fmt.Sprintf("ENTRYPOINT [\"%s\"]\n", strings.Join(stage.Entrypoint.Exec, "\",\"")),
)
if err != nil {
return err
}
err = RestoreWorkingDirectory(stage.Entrypoint.Workdir, containerfile)
if err != nil {
return err
}
}
}
return nil
}
Build commands for each module in the recipe
{
cmds := []ModuleCommand{}
for _, moduleInterface := range modules {
var module Module
err := mapstructure.Decode(moduleInterface, &module)
if err != nil {
return nil, err
}
cmd, err := BuildModule(recipe, moduleInterface)
if err != nil {
return nil, err
}
cmds = append(cmds, ModuleCommand{
Name: module.Name,
Command: append(cmd, ""), // add empty entry to ensure proper newline in Containerfile
Workdir: module.Workdir,
})
}
return cmds, nil
}
{
var include IncludesModule
err := mapstructure.Decode(moduleInterface, &include)
if err != nil {
return "", err
}
if len(include.Includes) == 0 {
return "", errors.New("includes module must have at least one module to include")
}
var commands []string
for _, include := range include.Includes {
var modulePath string
// in case of a remote include, we need to download the
// recipe before including it
if include[:4] == "http" {
fmt.Printf("Downloading recipe from %s\n", include)
modulePath, err = downloadRecipe(include)
if err != nil {
return "", err
}
} else if followsGhPattern(include) {
// if the include follows the github pattern, we need to
// download the recipe from the github repository
fmt.Printf("Downloading recipe from %s\n", include)
modulePath, err = downloadGhRecipe(include)
if err != nil {
return "", err
}
} else {
modulePath = filepath.Join(recipe.ParentPath, include)
}
includeModule, err := GenModule(modulePath)
if err != nil {
return "", err
}
buildModule, err := BuildModule(recipe, includeModule)
if err != nil {
return "", err
}
commands = append(commands, buildModule...)
}
return strings.Join(commands, "\n"), nil
}
Build a command string for the given module in the recipe
{
var module Module
err := mapstructure.Decode(moduleInterface, &module)
if err != nil {
return []string{""}, err
}
fmt.Printf("Building module [%s] of type [%s]\n", module.Name, module.Type)
commands := []string{fmt.Sprintf("\n# Begin Module %s - %s", module.Name, module.Type)}
if len(module.Modules) > 0 {
for _, nestedModule := range module.Modules {
buildModule, err := BuildModule(recipe, nestedModule)
if err != nil {
return []string{""}, err
}
commands = append(commands, buildModule...)
}
}
moduleBuilders := map[string]func(interface{}, *api.Recipe) (string, error){
"shell": BuildShellModule,
"includes": buildIncludesModule,
}
if moduleBuilder, ok := moduleBuilders[module.Type]; ok {
command, err := moduleBuilder(moduleInterface, recipe)
if err != nil {
return []string{""}, err
}
commands = append(commands, command)
} else {
command, err := LoadBuildPlugin(module.Type, moduleInterface, recipe)
if err != nil {
return []string{""}, err
}
commands = append(commands, command...)
}
_ = os.MkdirAll(fmt.Sprintf("%s/%s", recipe.SourcesPath, module.Name), 0755)
dirInfo, err := os.Stat(filepath.Join(recipe.SourcesPath, module.Name))
if err != nil {
return []string{""}, err
}
if dirInfo.Size() > 0 {
commands = append([]string{fmt.Sprintf("ADD sources/%s /sources/%s", module.Name, module.Name)}, commands...)
commands = append(commands, fmt.Sprintf("RUN rm -rf /sources/%s", module.Name))
}
commands = append(commands, fmt.Sprintf("# End Module %s - %s\n", module.Name, module.Type))
fmt.Printf("Module [%s] built successfully\n", module.Name)
return commands, nil
}
Compile and build the recipe using the specified runtime
{
recipe, err := BuildRecipe(recipePath)
if err != nil {
return err
}
syscall.Seteuid(0)
syscall.Setegid(0)
switch runtime {
case "docker":
err = compileDocker(recipe, origGid, origUid)
if err != nil {
return err
}
case "podman":
err = compilePodman(recipe, origGid, origUid)
if err != nil {
return err
}
case "buildah":
return fmt.Errorf("buildah not implemented yet")
default:
return fmt.Errorf("no runtime specified and the prometheus library is not implemented yet")
}
syscall.Seteuid(origUid)
syscall.Setegid(origGid)
for _, finalizeInterface := range recipe.Finalize {
var module Finalize
err := mapstructure.Decode(finalizeInterface, &module)
if err != nil {
return err
}
err = LoadFinalizePlugin(module.Type, finalizeInterface, &recipe, runtime, isRoot, origGid, origUid)
if err != nil {
return err
}
}
fmt.Printf("Image %s built successfully using %s\n", recipe.Id, runtime)
return nil
}
Build an OCI image using the specified recipe through Docker
{
docker, err := exec.LookPath("docker")
if err != nil {
return err
}
cmd := exec.Command(
docker, "build",
"-t", fmt.Sprintf("localhost/%s", recipe.Id),
"-f", recipe.Containerfile,
".",
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = recipe.ParentPath
return cmd.Run()
}
Build an OCI image using the specified recipe through Podman
{
podman, err := exec.LookPath("podman")
if err != nil {
return err
}
cmd := exec.Command(
podman, "build",
"-t", fmt.Sprintf("localhost/%s", recipe.Id),
"-f", recipe.Containerfile,
".",
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = recipe.ParentPath
return cmd.Run()
}
Configuration for storage drivers
Retrieve the container storage configuration based on the runtime
{
storageconfig := &StorageConf{}
if runtime == "podman" {
podmanPath, err := exec.LookPath("podman")
out, err := exec.Command(
podmanPath, "info", "-f json").Output()
if err != nil {
fmt.Println("Failed to get podman info")
} else {
driver := strings.Split(strings.Split(string(out), "\"graphDriverName\": \"")[1], "\",")[0]
storageconfig.Driver = driver
graphRoot := strings.Split(strings.Split(string(out), "\"graphRoot\": \"")[1], "\",")[0]
storageconfig.Graphroot = graphRoot
runRoot := strings.Split(strings.Split(string(out), "\"runRoot\": \"")[1], "\",")[0]
storageconfig.Runroot = runRoot
}
}
if storageconfig.Runroot == "" {
storageconfig.Runroot = "/var/lib/vib/runroot"
storageconfig.Graphroot = "/var/lib/vib/graphroot"
storageconfig.Driver = "overlay"
}
store, err := cstorage.GetStore(cstorage.StoreOptions{
RunRoot: storageconfig.Runroot,
GraphRoot: storageconfig.Graphroot,
GraphDriverName: storageconfig.Driver,
})
if err != nil {
return store, err
}
return store, err
}
Retrieve the image ID for a given image name from the storage
{
images, err := store.Images()
if err != nil {
return "", err
}
for _, img := range images {
for _, imgname := range img.Names {
if imgname == name {
return img.ID, nil
}
}
}
return "", fmt.Errorf("image not found")
}
Retrieve the top layer ID for a given image ID from the storage
{
images, err := store.Images()
if err != nil {
return "", err
}
for _, img := range images {
if img.ID == imageid {
return img.TopLayer, nil
}
}
return "", fmt.Errorf("no top layer for id %s found", imageid)
}
Mount the image and return the mount directory
{
store, err := GetContainerStorage(runtime)
if err != nil {
return "", err
}
topLayerID, err := GetTopLayerID(imageid, store)
if err != nil {
return "", err
}
mountDir, err := store.Mount(topLayerID, "")
if err != nil {
return "", err
}
return mountDir, err
}
LoadRecipe loads a recipe from a file and returns a Recipe
Does not validate the recipe but it will catch some errors
a proper validation will be done in the future
{
recipe := &api.Recipe{}
// we use the absolute path to the recipe file as the
// root path for the recipe and all its files
recipePath, err := filepath.Abs(path)
if err != nil {
return nil, err
}
// here we open the recipe file and unmarshal it into
// the Recipe struct, this is not a full validation
// but it will catch some errors
recipeFile, err := os.Open(recipePath)
if err != nil {
return nil, err
}
defer recipeFile.Close()
recipeYAML, err := io.ReadAll(recipeFile)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(recipeYAML, recipe)
if err != nil {
return nil, err
}
// the recipe path is stored in the recipe itself
// for convenience
recipe.Path = recipePath
recipe.ParentPath = filepath.Dir(recipePath)
// assuming the Containerfile location is relative
recipe.Containerfile = filepath.Join(filepath.Dir(recipePath), "Containerfile")
err = os.RemoveAll(recipe.Containerfile)
if err != nil {
return nil, err
}
// we create the sources directory which is the place where
// all the sources will be stored and be available to all
// the modules
recipe.SourcesPath = filepath.Join(filepath.Dir(recipePath), "sources")
err = os.RemoveAll(recipe.SourcesPath)
if err != nil {
return nil, err
}
err = os.MkdirAll(recipe.SourcesPath, 0755)
if err != nil {
return nil, err
}
// the downloads directory is a transient directory, here all
// the downloaded sources will be stored before being moved
// to the sources directory. This is useful since some sources
// types need to be extracted, this way we can extract them
// directly to the sources directory after downloading them
recipe.DownloadsPath = filepath.Join(filepath.Dir(recipePath), "downloads")
err = os.RemoveAll(recipe.DownloadsPath)
if err != nil {
return nil, err
}
err = os.MkdirAll(recipe.DownloadsPath, 0755)
if err != nil {
return nil, err
}
// the plugins directory contains all plugins that vib can load
// and use for unknown modules in the recipe
recipe.PluginPath = filepath.Join(filepath.Dir(recipePath), "plugins")
// the includes directory is the place where we store all the
// files to be included in the container, this is useful for
// example to include configuration files. Each file must follow
// the File Hierarchy Standard (FHS) and be placed in the correct
// directory. For example, if you want to include a file in
// /etc/nginx/nginx.conf you must place it in includes/etc/nginx/nginx.conf
// so it will be copied to the correct location in the container
if len(strings.TrimSpace(recipe.IncludesPath)) == 0 {
recipe.IncludesPath = filepath.Join("includes.container")
}
_, err = os.Stat(recipe.IncludesPath)
if os.IsNotExist(err) {
err := os.MkdirAll(recipe.IncludesPath, 0755)
if err != nil {
return nil, err
}
}
for i, stage := range recipe.Stages {
// here we check if the extra Adds path exists
for _, add := range stage.Adds {
for src := range add.SrcDst {
fullPath := filepath.Join(filepath.Dir(recipePath), src)
_, err = os.Stat(fullPath)
if os.IsNotExist(err) {
return nil, err
}
}
}
recipe.Stages[i] = stage
}
return recipe, nil
}
downloadRecipe downloads a recipe from a remote URL and stores it to
a temporary file
{
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
tmpFile, err := os.CreateTemp("", "vib-recipe-")
if err != nil {
return "", err
}
defer tmpFile.Close()
_, err = io.Copy(tmpFile, resp.Body)
if err != nil {
return "", err
}
return tmpFile.Name(), nil
}
followsGhPattern checks if a given path follows the pattern:
gh:org/repo:branch:path
{
parts := strings.Split(s, ":")
if len(parts) != 4 {
return false
}
if parts[0] != "gh" {
return false
}
return true
}
downloadGhRecipe downloads a recipe from a github repository and stores it to
a temporary file
{
parts := strings.Split(gh, ":")
repo := parts[1]
branch := parts[2]
file := parts[3]
url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s", repo, branch, file)
return downloadRecipe(url)
}
GenModule generate a Module struct from a module path
{
var module map[string]interface{}
moduleFile, err := os.Open(modulePath)
if err != nil {
return module, err
}
defer moduleFile.Close()
moduleYAML, err := io.ReadAll(moduleFile)
if err != nil {
return module, err
}
err = yaml.Unmarshal(moduleYAML, &module)
if err != nil {
return module, err
}
return module, nil
}
TestRecipe validates a recipe by loading it and checking for errors
{
recipe, err := LoadRecipe(path)
if err != nil {
fmt.Printf("Error validating recipe: %s\n", err)
return nil, err
}
modules := 0
for _, stage := range recipe.Stages {
modules += len(stage.Modules)
}
fmt.Printf("Recipe %s validated successfully\n", recipe.Id)
fmt.Printf("Found %d stages\n", len(recipe.Stages))
fmt.Printf("Found %d modules\n", modules)
return recipe, nil
}
Configuration for shell modules
Build shell module commands and return them as a single string
Returns: Concatenated shell commands or an error if any step fails
{
var module ShellModule
err := mapstructure.Decode(moduleInterface, &module)
if err != nil {
return "", err
}
for _, source := range module.Sources {
if strings.TrimSpace(source.Type) != "" {
err := api.DownloadSource(recipe, source, module.Name)
if err != nil {
return "", err
}
err = api.MoveSource(recipe.DownloadsPath, recipe.SourcesPath, source, module.Name)
if err != nil {
return "", err
}
}
}
if len(module.Commands) == 0 {
return "", errors.New("no commands specified")
}
cmd := ""
for i, command := range module.Commands {
cmd += command
if i < len(module.Commands)-1 {
cmd += " && "
}
}
return "RUN " + cmd, nil
}
Configuration for a module
Configuration for finalization steps
Configuration for including other modules or recipes
Information for building a module
Configuration for a plugin
import "errors"
import "fmt"
import "os"
import "path/filepath"
import "strings"
import "github.com/mitchellh/mapstructure"
import "github.com/vanilla-os/vib/api"
import "fmt"
import "os"
import "os/exec"
import "syscall"
import "github.com/mitchellh/mapstructure"
import "github.com/vanilla-os/vib/api"
import "fmt"
import "github.com/containers/storage"
cstorage
import "os/exec"
import "strings"
import "fmt"
import "io"
import "net/http"
import "os"
import "path/filepath"
import "strings"
import "github.com/vanilla-os/vib/api"
import "gopkg.in/yaml.v3"
import "errors"
import "strings"
import "github.com/mitchellh/mapstructure"
import "github.com/vanilla-os/vib/api"
import "C"
import "github.com/vanilla-os/vib/api"