core
packageAPI reference for the core
package.
Imports
(16)CompileRecipe
Compile and build the recipe using the specified runtime
Parameters
Returns
func CompileRecipe(recipePath string, arch string, runtime string, isRoot bool, origGid int, origUid int, containerfilePath string) error
{
recipe, err := BuildRecipe(recipePath, arch, containerfilePath)
if err != nil {
return err
}
syscall.Setegid(0)
syscall.Seteuid(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.Setegid(origGid)
syscall.Seteuid(origUid)
for _, finalizeInterface := range recipe.Finalize {
var module Finalize
err := mapstructure.Decode(finalizeInterface, &module)
if err != nil {
return err
}
err = LoadFinalizePlugin(module.Type, finalizeInterface, &recipe, arch, runtime, isRoot, origGid, origUid)
if err != nil {
return err
}
}
fmt.Printf("Image %s built successfully using %s\n", recipe.Id, runtime)
return nil
}
compileDocker
Build an OCI image using the specified recipe through Docker
Parameters
Returns
func compileDocker(recipe api.Recipe, gid int, uid int) error
{
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()
}
compilePodman
Build an OCI image using the specified recipe through Podman
Parameters
Returns
func compilePodman(recipe api.Recipe, gid int, uid int) error
{
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()
}
StorageConf
Configuration for storage drivers
type StorageConf struct
Fields
| Name | Type | Description |
|---|---|---|
| Driver | string | |
| Runroot | string | |
| Graphroot | string |
GetContainerStorage
Retrieve the container storage configuration based on the runtime
Parameters
Returns
func GetContainerStorage(runtime string) (cstorage.Store, error)
{
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
}
GetImageID
Retrieve the image ID for a given image name from the storage
Parameters
Returns
func GetImageID(name string, store cstorage.Store) (string, error)
{
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")
}
GetTopLayerID
Retrieve the top layer ID for a given image ID from the storage
Parameters
Returns
func GetTopLayerID(imageid string, store cstorage.Store) (string, error)
{
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)
}
MountImage
Mount the image and return the mount directory
Parameters
Returns
func MountImage(imagename string, imageid string, runtime string) (string, error)
{
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
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
Parameters
Returns
func LoadRecipe(path string) (*api.Recipe, error)
{
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
}
if len(strings.TrimSpace(recipe.Vibversion)) <= 0 {
return nil, fmt.Errorf("version key not found in recipe file, assuming outdated recipe")
}
recipeVersionS := strings.Split(recipe.Vibversion, ".")
if len(recipeVersionS) != 3 {
return nil, fmt.Errorf("invalid version format, expected x.x.x, got %s", recipe.Vibversion)
}
recipeVersion := []uint8{0, 0, 0}
for i := 0; i < len(recipeVersion); i++ {
versionInt, err := strconv.ParseUint(recipeVersionS[i], 10, 8)
if err != nil {
return nil, err
}
if versionInt > math.MaxUint8 {
recipeVersion[i] = math.MaxUint8
} else {
recipeVersion[i] = uint8(versionInt)
}
}
if recipeVersion[0] < Min_Recipe_Version[0] || recipeVersion[1] < Min_Recipe_Version[1] || recipeVersion[2] < Min_Recipe_Version[2] {
return nil, fmt.Errorf("outdated recipe, this version of vib supports recipes starting at version %s", strings.Join(strings.Fields(fmt.Sprint(Min_Recipe_Version)), "."))
}
// the recipe path is stored in the recipe itself
// for convenience
recipe.Path = recipePath
recipe.ParentPath = filepath.Dir(recipePath)
// 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
downloadRecipe downloads a recipe from a remote URL and stores it to
a temporary file
Parameters
Returns
func downloadRecipe(url string) (path string, err error)
{
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
followsGhPattern checks if a given path follows the pattern:
gh:org/repo:branch:path
Parameters
Returns
func followsGhPattern(s string) bool
{
parts := strings.Split(s, ":")
if len(parts) != 4 {
return false
}
if parts[0] != "gh" {
return false
}
return true
}
downloadGhRecipe
downloadGhRecipe downloads a recipe from a github repository and stores it to
a temporary file
Parameters
Returns
func downloadGhRecipe(gh string) (path string, err error)
{
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
GenModule generate a Module struct from a module path
Parameters
Returns
func GenModule(modulePath string) (map[string]interface{}, error)
{
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
TestRecipe validates a recipe by loading it and checking for errors
Parameters
Returns
func TestRecipe(path string) (*api.Recipe, error)
{
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
}
ShellModule
Configuration for shell modules
type ShellModule struct
Fields
| Name | Type | Description |
|---|---|---|
| Name | string | json:"name" |
| Type | string | json:"type" |
| Sources | []api.Source | |
| Commands | []string | |
| Cleanup | []string |
BuildShellModule
Build shell module commands and return them as a single string
Parameters
Returns
func BuildShellModule(moduleInterface interface{}, recipe *api.Recipe, cleanup []string, arch string) (string, error)
{
var module ShellModule
err := mapstructure.Decode(moduleInterface, &module)
if err != nil {
return "", err
}
for _, source := range module.Sources {
if api.TestArch(source.OnlyArches, arch) {
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 += " && "
}
}
cmd += api.GetCleanupSuffix(append(cleanup, module.Cleanup...))
return fmt.Sprintf("RUN --mount=source=sources/%s,target=/sources/%s,rw ", module.Name, module.Name) + cmd, nil
}
Module
Configuration for a module
type Module struct
Fields
| Name | Type | Description |
|---|---|---|
| Name | string | json:"name" |
| Workdir | string | |
| Type | string | json:"type" |
| Modules | []map[string]interface{} | |
| Content | []byte | |
| Cleanup | []string | json:"cleanup" |
Finalize
Configuration for finalization steps
type Finalize struct
Fields
| Name | Type | Description |
|---|---|---|
| Name | string | json:"name" |
| Type | string | json:"type" |
| Content | []byte |
IncludesModule
Configuration for including other modules or recipes
type IncludesModule struct
Fields
| Name | Type | Description |
|---|---|---|
| Name | string | json:"name" |
| Type | string | json:"type" |
| Includes | []string | json:"includes" |
ModuleCommand
Information for building a module
type ModuleCommand struct
Fields
| Name | Type | Description |
|---|---|---|
| Name | string | |
| Command | []string | |
| Workdir | string |
Plugin
Configuration for a plugin
type Plugin struct
Fields
| Name | Type | Description |
|---|---|---|
| Name | string | |
| BuildFunc | func(*C.char, *C.char, *C.char) string | |
| LoadedPlugin | uintptr | |
| PluginInfo | api.PluginInfo |
ChangeWorkingDirectory
Add a WORKDIR instruction to the containerfile
Parameters
Returns
func ChangeWorkingDirectory(workdir string, containerfile *os.File) error
{
if workdir != "" {
_, err := containerfile.WriteString(
fmt.Sprintf("WORKDIR %s\n", workdir),
)
if err != nil {
return err
}
}
return nil
}
RestoreWorkingDirectory
Add a WORKDIR instruction to reset to the root directory
Parameters
Returns
func RestoreWorkingDirectory(workdir string, containerfile *os.File) error
{
if workdir != "" {
_, err := containerfile.WriteString(
fmt.Sprintf("WORKDIR %s\n", "/"),
)
if err != nil {
return err
}
}
return nil
}
BuildRecipe
Load and build a Containerfile from the specified recipe
Parameters
Returns
func BuildRecipe(recipePath string, arch string, containerfilePath string) (api.Recipe, error)
{
// load the recipe
recipe, err := LoadRecipe(recipePath)
if err != nil {
return api.Recipe{}, err
}
fmt.Printf("Building recipe %s\n", recipe.Name)
// assuming the Containerfile location is relative
if len(containerfilePath) == 0 {
recipe.Containerfile = filepath.Join(filepath.Dir(recipePath), "Containerfile")
} else {
recipe.Containerfile = filepath.Join(filepath.Dir(recipePath), containerfilePath)
fmt.Printf("Containerfile path: %s\n", recipe.Containerfile)
}
// build the Containerfile
err = BuildContainerfile(recipe, arch)
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
}
BuildContainerfile
Generate a Containerfile from the recipe
Parameters
Returns
func BuildContainerfile(recipe *api.Recipe, arch string) error
{
err := os.RemoveAll(recipe.Containerfile)
if err != nil {
return err
}
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, stage.Cleanup, arch)
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
}
cleanupSuffix := api.GetCleanupSuffix(stage.Cleanup)
for _, cmd := range stage.Runs.Commands {
_, err = containerfile.WriteString(
fmt.Sprintf("RUN %s\n", cmd + cleanupSuffix),
)
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
if stage.Addincludes {
_, 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
}
BuildModules
Build commands for each module in the recipe
Parameters
Returns
func BuildModules(recipe *api.Recipe, modules []interface{}, cleanup []string, arch string) ([]ModuleCommand, error)
{
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, cleanup, arch)
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
}
buildIncludesModule
Parameters
Returns
func buildIncludesModule(moduleInterface interface{}, recipe *api.Recipe, cleanup []string, arch string) (string, error)
{
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, cleanup, arch)
if err != nil {
return "", err
}
commands = append(commands, buildModule...)
}
return strings.Join(commands, "\n"), nil
}
BuildModule
Build a command string for the given module in the recipe
Parameters
Returns
func BuildModule(recipe *api.Recipe, moduleInterface interface{}, cleanup []string, arch string) ([]string, error)
{
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, append(cleanup, module.Cleanup...), arch)
if err != nil {
return []string{""}, err
}
commands = append(commands, buildModule...)
}
}
moduleBuilders := map[string]func(interface{}, *api.Recipe, []string, string) (string, error){
"shell": BuildShellModule,
"includes": buildIncludesModule,
}
if moduleBuilder, ok := moduleBuilders[module.Type]; ok {
command, err := moduleBuilder(moduleInterface, recipe, cleanup, arch)
if err != nil {
return []string{""}, err
}
commands = append(commands, command)
} else {
command, err := LoadBuildPlugin(module.Type, moduleInterface, recipe, cleanup, arch)
if err != nil {
return []string{""}, err
}
commands = append(commands, command...)
}
moduleSourcePath := filepath.Join(recipe.SourcesPath, module.Name)
err = os.MkdirAll(moduleSourcePath, 0755)
if err != nil {
return []string{""}, err
}
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
}