Browse Source

Proper support for volumes

Fixes
Implement OCI standard fields in configuration format
Make initial configuration structs internal and add the oci spec strucs as children as they do not support all features
Add Volumes json data as seperate blobs to the oci image
master
Till Wegmueller 3 years ago
parent
commit
1753dc8cce
  1. 10
      Taskfile.yml
  2. 62
      Vagrantfile
  3. 1
      cmd/imageadm/root.go
  4. 6
      cmd/podadm/create.go
  5. 36
      cmd/podadm/login.go
  6. 88
      host/build.go
  7. 57
      host/container.go
  8. 20
      image/build/config.go
  9. 22
      image/image.go
  10. 60
      image/oci/metadata_compat.go
  11. 104
      image/oci/writer.go
  12. 4
      image/transport/aci.go
  13. 8
      image/transport/docker.go
  14. 6
      image/update.go
  15. 110
      pod/container.go
  16. 89
      pod/manifest.go
  17. 40
      pod/zone.go
  18. 10
      supportfiles/volume.hcl
  19. 47
      volume/volume.go

10
Taskfile.yml

@ -7,7 +7,7 @@ env:
vars:
VERSION: {sh: git describe --tags --always}
GOARCH: "amd64"
GOOS: "solaris"
GOOS: "illumos"
#Required by package tools to set install into the temporary root folder instead of /
DESTDIR: ""
#Prefix to install packages into
@ -37,7 +37,6 @@ tasks:
cmds:
- task: imageadm
- task: buildhelper
vars: {GOOS: "solaris"}
- task: buildhelper
vars: {GOOS: "linux"}
- task: podadm
@ -53,11 +52,12 @@ tasks:
deps: [build]
cmds:
- "mkdir -p {{.DESTDIR}}/{{.PREFIX}}"
- "{{.GCP}} -t {{.DESTDIR}}/{{.PREFIX}} artifacts/imageadm artifacts/podadm"
- "mkdir -p {{.DESTDIR}}/{{.BRAND_DIR}}/pod/helpers/solaris"
- "mkdir -p {{.DESTDIR}}/{{.PREFIX}}/bin"
- "{{.GCP}} -t {{.DESTDIR}}/{{.PREFIX}}/bin/ artifacts/imageadm artifacts/podadm"
- "mkdir -p {{.DESTDIR}}/{{.BRAND_DIR}}/pod/helpers/{{.GOOS}}"
- "mkdir -p {{.DESTDIR}}/{{.BRAND_DIR}}/pod/helpers/linux"
- "{{.GCP}} -t {{.DESTDIR}}/{{.BRAND_DIR}}/pod supportfiles/brand/*"
- "{{.GCP}} artifacts/buildhelper_solaris {{.DESTDIR}}/{{.BRAND_DIR}}/pod/helpers/solaris/buildhelper"
- "{{.GCP}} artifacts/buildhelper_{{.GOOS}} {{.DESTDIR}}/{{.BRAND_DIR}}/pod/helpers/{{.GOOS}}/buildhelper"
- "{{.GCP}} artifacts/buildhelper_linux {{.DESTDIR}}/{{.BRAND_DIR}}/pod/helpers/linux/buildhelper"
tools:

62
Vagrantfile vendored

@ -1,54 +1,7 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
# Every Vagrant development environment requires a box. You can search for
# boxes at https://vagrantcloud.com/search.
config.vm.box = "openindiana/hipster"
# Disable automatic box update checking. If you disable this, then
# boxes will only be checked for updates when the user runs
# `vagrant box outdated`. This is not recommended.
# config.vm.box_check_update = false
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine. In the example below,
# accessing "localhost:8080" will access port 80 on the guest machine.
# NOTE: This will enable public access to the opened port
# config.vm.network "forwarded_port", guest: 80, host: 8080
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine and only allow access
# via 127.0.0.1 to disable public access
# config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"
# Create a private network, which allows host-only access to the machine
# using a specific IP.
# config.vm.network "private_network", ip: "192.168.33.10"
# Create a public network, which generally matched to bridged network.
# Bridged networks make the machine appear as another physical device on
# your network.
# config.vm.network "public_network"
# Share an additional folder to the guest VM. The first argument is
# the path on the host to the actual folder. The second argument is
# the path on the guest to mount the folder. And the optional third
# argument is a set of non-required options.
# config.vm.synced_folder "../data", "/vagrant_data"
# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
config.vm.box = "Toasterson/openindiana-hipster"
config.vm.provider "virtualbox" do |vb|
# Set name of VM in virtualbox"
vb.name = "opencloud_dev"
@ -58,13 +11,6 @@ Vagrant.configure("2") do |config|
vb.cpus = 4
vb.memory = 4096
end
#
# View the documentation for the provider you are using for more
# information on available options.
# Enable provisioning with a shell script. Additional provisioners such as
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
# documentation for more information about their specific syntax and use.
config.vm.provision "shell", inline: <<-SHELL
# Install the go compiler and system header files to compile the applications inside the zone
@ -101,7 +47,7 @@ EOF
zoneadm -z template install
echo "Installing developer tools"
pkg install -v golang-113 system/header developer/build/gnu-make git developer/gcc-8 system/library/gcc-8-runtime jq
pkg install -v golang-114 system/header developer/build/gnu-make git developer/gcc-8 system/library/gcc-8-runtime jq
echo "Installing Task"
#curl -sL https://taskfile.dev/install.sh | sh
@ -109,9 +55,5 @@ EOF
pushd task/cmd/task
go install -v
popd
echo "Updating system inplace"
pkg update -v
SHELL
end

1
cmd/imageadm/root.go

@ -18,6 +18,7 @@ var RootCmd = &cobra.Command{
Long: `This is the Administrative Tool to Create and manage images
in an opencloud instance or whatever other application can use the images.
`,
SilenceUsage: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
viper.SetConfigName("opencloud")
viper.AddConfigPath("/etc/")

6
cmd/podadm/create.go

@ -21,7 +21,7 @@ var createCmd = &cobra.Command{
func init() {
RootCmd.AddCommand(createCmd)
f := createCmd.Flags()
f.StringArrayP("mount", "m", nil, "Add a volume to the container")
f.StringArrayP("volume", "v", nil, "Add a volume to the container")
f.StringP("name", "n", namesgenerator.GetRandomName(0), "Name for the container")
f.StringArrayP("label", "l", nil, "Labels of the Container")
f.String("network", net.DefaultNetworkName, "define to which network to attach the pod to")
@ -32,12 +32,12 @@ func createContainer(cmd *cobra.Command, args []string) error {
opt := &pod.Options{}
f := cmd.Flags()
var err error
opt.Name, err = cmd.Flags().GetString("name")
opt.Name, err = f.GetString("name")
if err != nil {
return tracerr.Wrap(err)
}
opt.MountSpecs, err = f.GetStringArray("mount")
opt.MountSpecs, err = f.GetStringArray("volume")
if err != nil {
return tracerr.Wrap(err)
}

36
cmd/podadm/login.go

@ -0,0 +1,36 @@
package podadm
import (
"os"
"os/exec"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/ztrue/tracerr"
)
var loginCmd = &cobra.Command{
Use: "login",
Short: "Login to the container or exec the command",
Aliases: []string{"exec"},
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := h.FindContainer(args[0])
if err != nil {
return tracerr.Wrap(err)
}
zloginCmd := exec.Command("zlogin", append([]string{c.UUID.String()}, args[1:]...)...)
zloginCmd.Stdin = os.Stdin
zloginCmd.Stdout = os.Stdout
zloginCmd.Stderr = os.Stderr
return zloginCmd.Run()
},
}
func init() {
RootCmd.AddCommand(loginCmd)
f := startCmd.Flags()
if err := viper.BindPFlags(f); err != nil {
panic(err)
}
}

88
host/build.go

@ -5,16 +5,20 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"git.wegmueller.it/illumos/go-zone"
"git.wegmueller.it/opencloud/opencloud/image"
"git.wegmueller.it/opencloud/opencloud/image/build"
"git.wegmueller.it/opencloud/opencloud/image/oci"
"git.wegmueller.it/opencloud/opencloud/image/reference"
namesgenerator "git.wegmueller.it/opencloud/opencloud/namegenerator"
opcNet "git.wegmueller.it/opencloud/opencloud/net"
"git.wegmueller.it/opencloud/opencloud/pod"
"git.wegmueller.it/opencloud/opencloud/volume"
"github.com/docker/docker/pkg/fileutils"
"github.com/dustin/go-humanize"
specsv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
"github.com/ztrue/tracerr"
@ -97,7 +101,7 @@ func (h *Host) RunBuild(cfg *build.ImageConfig, opts *pod.Options) error {
func (h *Host) CreateBuildContainer(config build.Image, options *pod.Options) (*pod.Container, error) {
name := namesgenerator.GetRandomName(5)
name := "build-" + namesgenerator.GetRandomName(5)
ref := reference.NewReferenceString(config.BaseImage)
logrus.Debugf("creating build container for %s", config.Name)
// Either create a container from a base image
@ -112,7 +116,7 @@ func (h *Host) CreateBuildContainer(config build.Image, options *pod.Options) (*
if err = img.Load(); err != nil {
return nil, tracerr.Wrap(err)
}
pm, err := pod.ContainerManifestFromImage(ref, img)
m, err := pod.ContainerManifestFromImage(ref, img)
if err != nil {
return nil, tracerr.Wrap(err)
}
@ -124,8 +128,8 @@ func (h *Host) CreateBuildContainer(config build.Image, options *pod.Options) (*
if err != nil {
return nil, tracerr.Wrap(err)
}
pm.Solaris.Anet = []specs.SolarisAnet{anet}
container, err := pod.CreateContainer(h.Dataset, pm, name, ref.Tag, img)
m.Spec.Solaris.Anet = []specs.SolarisAnet{anet}
container, err := pod.CreateContainer(h.Dataset, m, name, ref.Tag, img)
if err != nil {
return nil, tracerr.Wrap(err)
}
@ -137,7 +141,7 @@ func (h *Host) CreateBuildContainer(config build.Image, options *pod.Options) (*
} else {
//or make an new empty one if the base image is image.EmptyImageName
logrus.Debug("creating an empty container")
pm := pod.EmptyContainerManifest()
m := pod.EmptyContainerManifest()
net, err := opcNet.GetNetwork(options.Network)
if err != nil {
return nil, tracerr.Wrap(err)
@ -146,8 +150,8 @@ func (h *Host) CreateBuildContainer(config build.Image, options *pod.Options) (*
if err != nil {
return nil, tracerr.Wrap(err)
}
pm.Solaris.Anet = []specs.SolarisAnet{anet}
container, err := pod.CreateEmptyContainer(h.Dataset, pm, name)
m.Spec.Solaris.Anet = []specs.SolarisAnet{anet}
container, err := pod.CreateEmptyContainer(h.Dataset, m, name)
if err != nil {
return nil, tracerr.Wrap(err)
}
@ -218,6 +222,7 @@ func (h *Host) RunBuildHelperInContainer(config build.Image, container *pod.Cont
func (h *Host) ExportToImage(imageName reference.Reference, container *pod.Container, imageConfig build.Image) (*image.Image, error) {
//First lets search if we already have the same image with a new tag.
var i *image.Image
var wr *oci.Writer
//First load existing image with existing tags
if h.HasImage(imageName) {
logrus.Debugf("loading existing base image %s", imageName.String())
@ -230,8 +235,8 @@ func (h *Host) ExportToImage(imageName reference.Reference, container *pod.Conta
//Get Parent image and top layer
if imageConfig.BaseImage == "" || imageConfig.BaseImage == image.EmptyImageName {
var err error
if i == nil {
var err error
i, err = image.CreateEmptyImage(imageName, nil)
if err != nil {
return nil, tracerr.Wrap(err)
@ -239,7 +244,7 @@ func (h *Host) ExportToImage(imageName reference.Reference, container *pod.Conta
}
//Create a OCI Image Writer
logrus.Debugf("Creating OCI Image for %s", imageName)
wr, err := i.NewOCIWriter()
wr, err = i.NewOCIWriter()
if err != nil {
return nil, tracerr.Wrap(err)
}
@ -251,10 +256,7 @@ func (h *Host) ExportToImage(imageName reference.Reference, container *pod.Conta
if err = wr.AddTree(container.Path("root")); err != nil {
return nil, tracerr.Wrap(err)
}
logrus.Info("successful")
if err = wr.Close(); err != nil {
return nil, tracerr.Wrap(err)
}
} else {
parentRef := reference.NewReferenceString(imageConfig.BaseImage)
logrus.Infof("Creating diff between %s and %s", parentRef, imageName)
@ -285,11 +287,13 @@ func (h *Host) ExportToImage(imageName reference.Reference, container *pod.Conta
//Create a OCI Image Writer
logrus.Debugf("Creating OCI Image for %s", imageName)
wr, err := i.NewOCIWriter()
wr, err = i.NewOCIWriter()
if err != nil {
return nil, tracerr.Wrap(err)
}
wr.AddParentLayers(tg.Layers)
if err = wr.NewLayer(); err != nil {
return nil, tracerr.Wrap(err)
}
@ -298,15 +302,45 @@ func (h *Host) ExportToImage(imageName reference.Reference, container *pod.Conta
if err != nil {
return nil, tracerr.Wrap(err)
}
}
wr.AddParentLayers(tg.Layers)
specImageConfig := specsv1.ImageConfig{
User: imageConfig.ServiceUser,
ExposedPorts: toStringStruct(imageConfig.ExposedPorts),
Env: toStringSlice(imageConfig.Environment, "="),
Entrypoint: imageConfig.Enytrpoints,
Cmd: imageConfig.Command,
WorkingDir: imageConfig.WorkingDir,
Labels: imageConfig.Labels,
}
logrus.Info("successful")
if err = wr.Close(); err != nil {
return nil, tracerr.Wrap(err)
wr.AddImageConfig(specImageConfig)
logrus.Infof("saving volume metadata")
for i, volBuildConfig := range imageConfig.Volumes {
vol := volume.Config{
Name: volBuildConfig.Name,
MountPath: volBuildConfig.Path,
Persist: volBuildConfig.Persist,
Backup: volBuildConfig.Backup,
Properties: volBuildConfig.Properties,
}
if vol.Name == "" {
vol.Name = fmt.Sprintf("volume-%d", i)
}
if err := wr.AddMetadata(volume.MetadataKeyPrefix+strings.ReplaceAll(vol.Name, "/", ".")+oci.AnnotationMetadataBlobSuffix, vol); err != nil {
return nil, fmt.Errorf("could not export image %s: failed to save volume %s: %w", imageConfig.Name, vol.Name, err)
}
}
logrus.Infof("finishing image")
if err := wr.Close(); err != nil {
return nil, tracerr.Wrap(err)
}
logrus.Info("successfully saved oci image")
logrus.Info("importing OCI image into dataset")
if err := i.ImportOCILayers(true); err != nil {
return nil, tracerr.Wrap(err)
@ -315,3 +349,21 @@ func (h *Host) ExportToImage(imageName reference.Reference, container *pod.Conta
return i, nil
}
func toStringStruct(stringSlice []string) map[string]struct{} {
retVal := make(map[string]struct{})
for _, str := range stringSlice {
retVal[str] = struct{}{}
}
return retVal
}
func toStringSlice(stringMap map[string]string, seperator string) []string {
retVal := make([]string, len(stringMap))
i := 0
for key, value := range stringMap {
retVal[i] = fmt.Sprintf("%s%s%s", key, seperator, value)
i++
}
return retVal
}

57
host/container.go

@ -9,8 +9,6 @@ import (
"git.wegmueller.it/opencloud/opencloud/image/reference"
opcNet "git.wegmueller.it/opencloud/opencloud/net"
"git.wegmueller.it/opencloud/opencloud/pod"
volume "git.wegmueller.it/opencloud/opencloud/volume"
"git.wegmueller.it/opencloud/opencloud/zfs"
"github.com/labstack/gommon/log"
"github.com/opencontainers/runtime-spec/specs-go"
uuid "github.com/satori/go.uuid"
@ -39,14 +37,14 @@ func (h *Host) CreateContainer(ref reference.Reference, opt *pod.Options) (*pod.
return nil, tracerr.Wrap(err)
}
pm, err := pod.ContainerManifestFromImage(ref, img)
m, err := pod.ContainerManifestFromImage(ref, img)
if err != nil {
return nil, tracerr.Wrap(err)
}
for _, mntDesc := range opt.MountSpecs {
mnt := pod.NewMountString(mntDesc)
pm.Mounts = append(pm.Mounts, mnt)
m.Spec.Mounts = append(m.Spec.Mounts, mnt)
}
for _, l := range opt.Labels {
@ -56,7 +54,7 @@ func (h *Host) CreateContainer(ref reference.Reference, opt *pod.Options) (*pod.
continue
}
k, v := l[:idx], l[idx+1:]
pm.Annotations[k] = v
m.Spec.Annotations[k] = v
}
net, err := opcNet.GetNetwork(opt.Network)
@ -67,58 +65,13 @@ func (h *Host) CreateContainer(ref reference.Reference, opt *pod.Options) (*pod.
if err != nil {
return nil, tracerr.Wrap(err)
}
pm.Solaris.Anet = []specs.SolarisAnet{anet}
m.Spec.Solaris.Anet = []specs.SolarisAnet{anet}
container, err := pod.CreateContainer(h.Dataset, pm, opt.Name, ref.Tag, img)
container, err := pod.CreateContainer(h.Dataset, m, opt.Name, ref.Tag, img)
if err != nil {
return nil, tracerr.Wrap(err)
}
for i, mount := range container.Manifest.Mounts {
if mount.Type == "volume" {
if mount.Destination == "" {
return nil, fmt.Errorf("emtpy volume destination for volume %d", i)
}
if mount.Source == "" {
mount.Source = fmt.Sprintf("volume.%d", i)
} else if strings.Contains(mount.Source, "/") {
idx := strings.LastIndex(mount.Source, "/")
mount.Source = mount.Source[idx+1:]
}
volConf := volume.Config{
Name: mount.Source,
MountPath: mount.Destination,
Persist: false,
Backup: false,
}
for _, opt := range mount.Options {
switch opt {
case "persistent":
volConf.Persist = true
case "backup":
volConf.Backup = true
}
}
var pds *zfs.Dataset
if !volConf.Persist {
pds = container.GetDataset()
}
vol, err := volume.CreateVolume(volConf, pds)
if err != nil {
return nil, tracerr.Wrap(err)
}
mount.Source = vol.GetDatasetPath()
mount.Type = "dataset"
container.Manifest.Mounts[i] = mount
}
}
if err = h.setupDNSInZone(container, net); err != nil {
return nil, tracerr.Wrap(err)
}

20
image/build/config.go

@ -50,12 +50,16 @@ type Image struct {
CopyOperations []CopyOperation `hcl:"copy,block"`
Actions []Action `hcl:"action,block"`
Maintainer *MaintainerInformation `hcl:"maintainer,optional"`
Services []ServiceDescription `hcl:"service,block"`
InitCommand []string `hcl:"cmd,optional"`
Services []ServiceDescription `hcl:"service,block"` //TODO implement
Command []string `hcl:"cmd,optional"` //TODO implement with something like gvisor
ExposedPorts []string `hcl:"expose,optional"`
Environment map[string]string `hcl:"env,optional"`
Enytrpoints []string `hcl:"entrypoints,optional"`
Volumes []VolumeConfig `hcl:"volume,block"`
Hosts []HostEntry `hcl:"host,block"`
Hosts []HostEntry `hcl:"host,block"` //TODO impelement
WorkingDir string `hcl:"workdir,optional"` //TODO implement
ServiceUser string `hcl:"user,optional"` //TODO implement
Labels map[string]string `hcl:"labels,optional"`
}
type ServiceDescription struct {
@ -86,10 +90,12 @@ type User struct {
}
type VolumeConfig struct {
Name string `hcl:"name,optional"`
Path string `hcl:"path,label"`
Persist bool `hcl:"persist,optional"`
Properties zfs.Properties `hcl:"properties,optional"`
Name string `hcl:"name,optional"`
Path string `hcl:"path,label"`
Persist bool `hcl:"persist,optional"`
Backup bool `hcl:"backup,optional"`
Properties zfs.Properties `hcl:"properties,optional"`
MountOptions []string `hcl:"mount_options,optional"`
}
type MaintainerInformation struct {

22
image/image.go

@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"strconv"
@ -95,9 +94,8 @@ func (img *Image) HasLayer(name digest.Digest) bool {
return false
}
func (img *Image) GetManifestFromDisk(digest digest.Digest) (manifest *specsv1.Manifest, err error) {
manPath := path.Join(img.VFSPath(), config.BlobsDirectory, digest.Algorithm().String(), digest.Encoded())
manBlob, err := ioutil.ReadFile(manPath)
func (img *Image) GetManifestFromDisk(manifestDigest digest.Digest) (manifest *specsv1.Manifest, err error) {
manBlob, err := img.GetBlob(manifestDigest)
if err != nil {
return nil, tracerr.Wrap(err)
}
@ -107,9 +105,8 @@ func (img *Image) GetManifestFromDisk(digest digest.Digest) (manifest *specsv1.M
return manifest, nil
}
func (img *Image) GetConfig(digest digest.Digest) (cfg *specsv1.Image, err error) {
cfgPath := path.Join(img.VFSPath(), config.BlobsDirectory, digest.Algorithm().String(), digest.Encoded())
cfgBlob, err := ioutil.ReadFile(cfgPath)
func (img *Image) GetConfig(configDigest digest.Digest) (cfg *specsv1.Image, err error) {
cfgBlob, err := img.GetBlob(configDigest)
if err != nil {
return nil, tracerr.Wrap(err)
}
@ -119,6 +116,15 @@ func (img *Image) GetConfig(digest digest.Digest) (cfg *specsv1.Image, err error
return cfg, nil
}
func (img *Image) GetBlob(blobDigest digest.Digest) ([]byte, error) {
p := path.Join(img.VFSPath(), config.BlobsDirectory, blobDigest.Algorithm().String(), blobDigest.Encoded())
blob, err := ioutil.ReadFile(p)
if err != nil {
return nil, tracerr.Wrap(err)
}
return blob, nil
}
func (img *Image) GetSize() int64 {
used := img.imagesDS.Properties[zfs.PropertyUsed]
v, err := strconv.ParseInt(used.Value, 10, 64)
@ -148,7 +154,7 @@ func CreateImage(ref reference.Reference, dataset *zfs.Dataset) (*Image, error)
if err != nil {
return nil, tracerr.Wrap(err)
}
err = img.Update(ref, os.Stdout)
err = img.Update(ref)
if err != nil {
return nil, tracerr.Wrap(err)
}

60
image/oci/metadata_compat.go

@ -0,0 +1,60 @@
package oci
import (
"encoding/json"
"fmt"
"io/ioutil"
"github.com/opencontainers/go-digest"
)
const (
AnnotationMetadataBlobSuffix = ".metadata.blob"
)
func (w *Writer) AddMetadata(label string, data interface{}) error {
if label == "" {
panic(fmt.Errorf("programmer error label for metadata not set"))
}
blob, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("could not marshal medatata %s: %w", label, err)
}
metadataDigest := digest.NewDigestFromBytes(w.algorithm, blob)
if err := ioutil.WriteFile(w.GetBlobPath(metadataDigest.Encoded()), blob, 0644); err != nil {
return fmt.Errorf("could not write extra blob file %s: %w", metadataDigest, err)
}
w.manifest.Annotations[label] = metadataDigest.String()
return nil
}
// Note this function assumes that you have downloaded the additional blobs from the registry
func (r *Reader) GetMetadata(manifestDigest digest.Digest, label string, targetDataPtr interface{}) error {
if label == "" {
panic(fmt.Errorf("programmer error label for metadata not set: cannot read empty string as metadata"))
}
manifest, err := r.GetManifest(manifestDigest)
if err != nil {
return fmt.Errorf("could not load manifest with digest %s: %w", manifestDigest, err)
}
metadataDigestString, ok := manifest.Annotations[label]
if !ok {
return fmt.Errorf("not metadata for label %s in index", label)
}
metadataDigest := digest.Digest(metadataDigestString)
blob, err := ioutil.ReadFile(r.GetBlobPath(metadataDigest.Algorithm(), metadataDigest.Encoded()))
if err != nil {
return fmt.Errorf("could metadata file with digest %s from blob store: %w", metadataDigest, err)
}
return json.Unmarshal(blob, targetDataPtr)
}

104
image/oci/writer.go

@ -3,6 +3,7 @@ package oci
import (
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
@ -22,7 +23,7 @@ const ManifestTempFilePattern = ".oci.manifest-temp."
const ConfigTempFilePattern = ".oci.config-temp."
// Make a OCI Image writer which writes the OCI Directory Layout acording to information given
// AddConfig(Image) -- Add a config Blob
// WriteConfigToFile(Image) -- Add a config Blob
// NewLayer() -- Advance the Writer to a new Layer Close Existing LayerWriter
// AddLayerFile(disgest Disgest) -- Add an Existing Layer
// AddFile(path, FileInfo, *os.File) error -- add a file to the current layer
@ -30,21 +31,27 @@ const ConfigTempFilePattern = ".oci.config-temp."
// AddDiff(layer1 string, layer2 string) error -- Add a diff between layer1 and 2 to the layer (including Whiteout)
// Close() -- Write Manifest and index to disk finishing the directory layout
type Writer struct {
clw *LayerWriter
targetDir string
algorithm digest.Algorithm
index *specsv1.Index
cfg specsv1.Descriptor
writtenLayers []specsv1.Descriptor
annotations map[string]string
parentLayers []specsv1.Descriptor
imageConfig specsv1.ImageConfig
clw *LayerWriter
targetDir string
algorithm digest.Algorithm
manifest *specsv1.Manifest
index *specsv1.Index
platform specsv1.Platform
imageConfig specsv1.ImageConfig
}
func NewOCIWriter(targetDir string, algorithm digest.Algorithm) (w *Writer, err error) {
w = &Writer{
targetDir: targetDir,
index: &specsv1.Index{},
index: &specsv1.Index{
Manifests: make([]specsv1.Descriptor, 0),
Annotations: make(map[string]string),
},
manifest: &specsv1.Manifest{
Versioned: specs.Versioned{SchemaVersion: 2},
Layers: make([]specsv1.Descriptor, 0),
Annotations: make(map[string]string),
},
}
if w.algorithm == "" {
w.algorithm = digest.Canonical
@ -82,7 +89,7 @@ func (w *Writer) SaveIndex() error {
}
func (w *Writer) AddParentLayers(l []specsv1.Descriptor) {
w.parentLayers = l
w.manifest.Layers = append(l, w.manifest.Layers...)
}
func (w *Writer) GetConfigDescriptor() specsv1.Descriptor {
@ -129,7 +136,7 @@ func (w *Writer) GetBlobPath(encoded string) string {
return filepath.Join(w.targetDir, "blobs", w.algorithm.String(), encoded)
}
func (w *Writer) AddConfig(config *specsv1.Image) error {
func (w *Writer) WriteConfigToFile(config *specsv1.Image) error {
tmpFile, err := ioutil.TempFile(w.GetBlobPath(""), ConfigTempFilePattern)
if err != nil {
return tracerr.Wrap(err)
@ -142,7 +149,7 @@ func (w *Writer) AddConfig(config *specsv1.Image) error {
if err != nil {
return tracerr.Wrap(err)
}
w.cfg = specsv1.Descriptor{
w.manifest.Config = specsv1.Descriptor{
MediaType: specsv1.MediaTypeImageConfig,
Digest: digest.NewDigest(w.algorithm, h),
Size: stat.Size(),
@ -151,9 +158,11 @@ func (w *Writer) AddConfig(config *specsv1.Image) error {
Architecture: runtime.GOARCH,
},
}
if err := os.Rename(tmpFile.Name(), w.GetBlobPath(w.cfg.Digest.Encoded())); err != nil {
if err := os.Rename(tmpFile.Name(), w.GetBlobPath(w.manifest.Config.Digest.Encoded())); err != nil {
return tracerr.Wrap(err)
}
return nil
}
@ -189,7 +198,7 @@ func (w *Writer) AddLayerFile(origPath string) error {
},
}
w.writtenLayers = append(w.writtenLayers, desc)
w.manifest.Layers = append(w.manifest.Layers, desc)
return os.Rename(tmpFile.Name(), w.GetBlobPath(desc.Digest.Encoded()))
}
@ -224,7 +233,7 @@ func (w *Writer) AddTree(rPath string) error {
return tracerr.Wrap(err)
}
w.writtenLayers = append(w.writtenLayers, w.clw.GetDescriptor())
w.manifest.Layers = append(w.manifest.Layers, w.clw.GetDescriptor())
w.clw = nil
return nil
@ -300,31 +309,40 @@ func (w *Writer) AddDiff(layer1 string, layer2 string) error {
if err != nil {
return tracerr.Wrap(err)
}
w.writtenLayers = append(w.writtenLayers, w.clw.GetDescriptor())
w.manifest.Layers = append(w.manifest.Layers, w.clw.GetDescriptor())
w.clw = nil
return nil
}
func (w *Writer) AddAnnotation(key, value string) {
if w.annotations == nil {
w.annotations = make(map[string]string)
}
w.annotations[key] = value
w.manifest.Annotations[key] = value
}
func (w *Writer) AddIndexAnnotation(key, value string) {
w.index.Annotations[key] = value
}
func (w *Writer) AddImageConfig(cfg specsv1.ImageConfig) {
w.imageConfig = cfg
}
func (w *Writer) Close() error {
func (w *Writer) FlushLayer() error {
if w.clw != nil {
if err := w.clw.Close(); err != nil {
return tracerr.Wrap(err)
return fmt.Errorf("could not flush layer %s: %w", w.clw.GetDigest(), err)
}
w.writtenLayers = append(w.writtenLayers, w.clw.GetDescriptor())
w.manifest.Layers = append(w.manifest.Layers, w.clw.GetDescriptor())
w.clw = nil
}
return nil
}
func (w *Writer) Close() error {
if err := w.FlushLayer(); err != nil {
return fmt.Errorf("could not close writer: %w", err)
}
// Create Image configuration description
t := time.Now()
c := specsv1.Image{
Created: &t,
@ -338,11 +356,7 @@ func (w *Writer) Close() error {
Config: w.imageConfig,
}
for _, desc := range w.parentLayers {
c.RootFS.DiffIDs = append(c.RootFS.DiffIDs, desc.Digest)
}
for _, desc := range w.writtenLayers {
for _, desc := range w.manifest.Layers {
c.RootFS.DiffIDs = append(c.RootFS.DiffIDs, desc.Digest)
c.History = append(c.History, specsv1.History{
@ -352,26 +366,12 @@ func (w *Writer) Close() error {
})
}
if err := w.AddConfig(&c); err != nil {
// Save image configuration to disk
if err := w.WriteConfigToFile(&c); err != nil {
return tracerr.Wrap(err)
}
manifest := &specsv1.Manifest{
Versioned: specs.Versioned{SchemaVersion: 2},
Config: w.cfg,
Layers: append(w.parentLayers, w.writtenLayers...),
}
if w.annotations != nil {
manifest.Annotations = w.annotations
}
if w.index == nil {
w.index = &specsv1.Index{
Versioned: specs.Versioned{SchemaVersion: 2},
}
}
// Save manifest to disk
h := w.algorithm.Hash()
tmpFile, err := ioutil.TempFile(w.GetBlobPath(""), ManifestTempFilePattern)
if err != nil {
@ -379,7 +379,7 @@ func (w *Writer) Close() error {
}
defer tmpFile.Close()
err = json.NewEncoder(io.MultiWriter(h, tmpFile)).Encode(&manifest)
err = json.NewEncoder(io.MultiWriter(h, tmpFile)).Encode(&w.manifest)
if err != nil {
return tracerr.Wrap(err)
}
@ -403,14 +403,18 @@ func (w *Writer) Close() error {
Architecture: runtime.GOARCH,
},
}
if w.annotations != nil {
manDesc.Annotations = w.annotations
if len(w.manifest.Annotations) > 0 {
manDesc.Annotations = w.manifest.Annotations
}
w.index.Manifests = append(w.index.Manifests, manDesc)
// Save index to disk
if err = w.SaveIndex(); err != nil {
return tracerr.Wrap(err)
}
// Finalize by writing the layout file to disk
return w.WriteOCILayoutFile()
}

4
image/transport/aci.go

@ -1,11 +1,9 @@
package transport
import (
"os"
"git.wegmueller.it/opencloud/opencloud/image/reference"
)
func DownloadACIFiles(ref reference.Reference, target string, progessoutput *os.File) error {
func DownloadACIFiles(ref reference.Reference, target string) error {
panic("Not Implemented")
}

8
image/transport/docker.go

@ -23,7 +23,7 @@ import (
"github.com/ztrue/tracerr"
)
func PullDockerImage(ref reference.Reference, target string, progessOutput *os.File, skipList []digest.Digest) (*specsv1.Descriptor, error) {
func PullDockerImage(ref reference.Reference, target string, skipList []digest.Digest) (*specsv1.Descriptor, error) {
registryURL := viper.GetString(config.DockerHubURLConfigKey)
username := viper.GetString(config.DockerUsernameConfigKey)
password := viper.GetString(config.DockerPasswordConfigKey)
@ -43,10 +43,6 @@ func PullDockerImage(ref reference.Reference, target string, progessOutput *os.F
return nil, tracerr.Wrap(err)
}
if progessOutput == nil {
progessOutput = os.Stdout
}
// Grab the Manifest of the image reference (tag)
manifest, err := hub.ManifestV2(repo, ref.Tag)
if err != nil {
@ -55,8 +51,8 @@ func PullDockerImage(ref reference.Reference, target string, progessOutput *os.F
doneWG := new(sync.WaitGroup)
//TODO rewrite output to be a channel of updates rather than the progressbar directly
p := mpb.New(mpb.WithWidth(64), mpb.WithWaitGroup(doneWG))
// Download all layers with Progress Update support
for _, l := range manifest.Layers {
if _, err := os.Stat(filepath.Join(target, l.Digest.Algorithm().String())); os.IsNotExist(err) {

6
image/update.go

@ -16,7 +16,7 @@ import (
// Anything else will end up as layer in another image and not bother this image
// Say we have an image alpine:latest which is 3.9 at the time and then install another image based on alpine:3.8
// This image will not be downgraded, likewise you will not be able to provide any other tag than latest to downgrade this image
func (img *Image) Update(ref reference.Reference, progessOutput *os.File) (err error) {
func (img *Image) Update(ref reference.Reference) (err error) {
if ref.Scheme != reference.SchemeACI && ref.Scheme != reference.SchemeDocker {
return fmt.Errorf("image reference cannot be local for update")
}
@ -36,7 +36,7 @@ func (img *Image) Update(ref reference.Reference, progessOutput *os.File) (err e
if ref.Scheme == reference.SchemeDocker {
var manDesc *specsv1.Descriptor
var err error
if manDesc, err = transport.PullDockerImage(ref, targetDir, progessOutput, img.GetLayers()); err != nil {
if manDesc, err = transport.PullDockerImage(ref, targetDir, img.GetLayers()); err != nil {
return tracerr.Wrap(err)
}
@ -53,7 +53,7 @@ func (img *Image) Update(ref reference.Reference, progessOutput *os.File) (err e
}
if ref.Scheme == reference.SchemeACI {
if err := transport.DownloadACIFiles(ref, targetDir, progessOutput); err != nil {
if err := transport.DownloadACIFiles(ref, targetDir); err != nil {
return tracerr.Wrap(err)
}
//TODO Unpack ACI

110
pod/container.go

@ -31,27 +31,27 @@ const (
)
type Container struct {
status Status
dataset *zfs.Dataset
rootDS *zfs.Dataset
Name string `json:"name"`
UUID uuid.UUID `json:"uuid"`
Manifest spec.Spec `json:"manifest"`
Image imageSpec.Image `json:"image"`
ImageReference reference.Reference `json:"image_reference"`
Zone *config.Zone `json:"zone,omitempty"`
ImageLayerDesc imageSpec.Descriptor `json:"image_layer_desc"`
EphemeralVolumes []string `json:"ephemeral_volumes"`
PersistentVolumes []string `json:"persistent_volumes"`
lifecycleManager lifecycle.Lifecyclemanager
sealed bool
status Status
dataset *zfs.Dataset
rootDS *zfs.Dataset
Name string `json:"name"`
UUID uuid.UUID `json:"uuid"`
Manifest *ContainerManifest `json:"manifest"`
Image imageSpec.Image `json:"image"`
ImageReference reference.Reference `json:"image_reference"`
Zone *config.Zone `json:"zone,omitempty"`
ImageLayerDesc imageSpec.Descriptor `json:"image_layer_desc"`
Volumes map[string]volume.Config `json:"volumes"`
lifecycleManager lifecycle.Lifecyclemanager
sealed bool
}
func newContainer(manifest *spec.Spec, name string) *Container {
func newContainer(manifest *ContainerManifest, name string) *Container {
c := &Container{
Name: name,
UUID: uuid.NewV4(),
Manifest: *manifest,
Manifest: manifest,
Volumes: make(map[string]volume.Config),
}
return c
@ -61,8 +61,8 @@ func (c *Container) GetDataset() *zfs.Dataset {
return c.dataset
}
func CreateEmptyContainer(parentDataset *zfs.Dataset, pm *spec.Spec, name string) (container *Container, rErr error) {
container = newContainer(pm, name)
func CreateEmptyContainer(parentDataset *zfs.Dataset, m *ContainerManifest, name string) (container *Container, rErr error) {
container = newContainer(m, name)
if err := container.initDataset(parentDataset); err != nil {
return nil, tracerr.Wrap(err)
@ -87,8 +87,6 @@ func CreateEmptyContainer(parentDataset *zfs.Dataset, pm *spec.Spec, name string
return nil, tracerr.Wrap(err)
}
container.initNetworking()
if err := container.register(); err != nil {
return nil, err
}
@ -96,12 +94,13 @@ func CreateEmptyContainer(parentDataset *zfs.Dataset, pm *spec.Spec, name string
return container, nil
}
func CreateContainer(parentDataset *zfs.Dataset, pm *spec.Spec, name, tag string, img *image.Image) (container *Container, rErr error) {
func CreateContainer(parentDataset *zfs.Dataset, m *ContainerManifest, name, tag string, img *image.Image) (container *Container, rErr error) {
container = &Container{
UUID: uuid.NewV4(),
Manifest: *pm,
Manifest: m,
Name: name,
Volumes: make(map[string]volume.Config),
}
if err := container.initDataset(parentDataset); err != nil {
@ -119,7 +118,7 @@ func CreateContainer(parentDataset *zfs.Dataset, pm *spec.Spec, name, tag string
return nil, tracerr.Wrap(err)
}
container.Manifest.Root = &spec.Root{
container.Manifest.Spec.Root = &spec.Root{
Path: container.dataset.Mountpoint,
}
@ -135,8 +134,6 @@ func CreateContainer(parentDataset *zfs.Dataset, pm *spec.Spec, name, tag string
return nil, tracerr.Wrap(err)
}
container.initNetworking()
if err := container.register(); err != nil {
return nil, err
}
@ -153,7 +150,7 @@ func (c *Container) openImage(tag string, img *image.Image) error {
if err != nil {
return tracerr.Wrap(err)
}
if c.Manifest.Process == nil && (cfg.Config.Entrypoint == nil || cfg.Config.Cmd == nil) {
if c.Manifest.Spec.Process == nil && (cfg.Config.Entrypoint == nil || cfg.Config.Cmd == nil) {
return errors.New("container has no command or apps to start")
}
@ -233,56 +230,19 @@ func (c *Container) register() (err error) {
return nil
}
func (c *Container) initNetworking() {
if c.Manifest.Solaris != nil {
for _, anet := range c.Manifest.Solaris.Anet {
c.Zone.Networks = append(c.Zone.Networks, config.Network{
AllowedAddress: anet.Allowedaddr,
Physical: anet.Linkname,
Defrouter: anet.Defrouter,
})
}
}
}
func (c *Container) initVolumes() error {
if len(c.Manifest.Mounts) > 0 {
for i, mount := range c.Manifest.Mounts {
if mount.Destination == "" {
return fmt.Errorf("emtpy volume destination for volume %d", i)
}
switch mount.Type {
case "ephemeral":
volConf := volume.Config{
Name: mount.Source,
MountPath: mount.Destination,
Persist: false,
Backup: false,
}
vol, err := volume.CreateVolume(volConf, c.dataset)
if err != nil {
return tracerr.Wrap(err)
}
c.EphemeralVolumes = append(c.EphemeralVolumes, vol.GetDatasetPath())
case "persistent":
volConf := volume.Config{
Name: mount.Source,
MountPath: mount.Destination,
Persist: true,
Backup: true,
}
vol, err := volume.CreateVolume(volConf, c.dataset)
if err != nil {
return tracerr.Wrap(err)
}
if err := volume.PersistVolume(c.UUID, vol); err != nil {
return tracerr.Wrap(err)
}
c.PersistentVolumes = append(c.PersistentVolumes, vol.GetDatasetPath())
}
for i, volCfg := range c.Manifest.Volumes {
if volCfg.Name == "" {
volCfg.Name = fmt.Sprintf("volume.%d", i)
}
vol, err := volume.CreateVolume(volCfg, c.dataset, c.UUID)
if err != nil {
return fmt.Errorf("could not initialize volume for container %s(%s): %w", c.UUID, c.Name, err)
}
c.Volumes[vol.GetDatasetPath()] = volCfg
}
return nil
@ -295,10 +255,6 @@ func (c *Container) initDataset(parentDataset *zfs.Dataset) error {
}
c.dataset = ds
c.Manifest.Root = &spec.Root{
Path: ds.Mountpoint,
}
return nil
}

89
pod/manifest.go

@ -1,79 +1,96 @@
package pod
import (
"encoding/json"
"fmt"
"strings"
"git.wegmueller.it/opencloud/opencloud/image"
"git.wegmueller.it/opencloud/opencloud/image/reference"
"git.wegmueller.it/opencloud/opencloud/volume"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/ztrue/tracerr"
)
func EmptyContainerManifest() (pm *specs.Spec) {
pm = &specs.Spec{}
pm.Root = &specs.Root{
Path: "root",
Readonly: false,
type ContainerManifest struct {
Spec specs.Spec
Volumes []volume.Config
}
func EmptyContainerManifest() *ContainerManifest {
return &ContainerManifest{
Spec: specs.Spec{
Root: &specs.Root{
Path: "root",
Readonly: false,
},
Solaris: &specs.Solaris{},
},
Volumes: make([]volume.Config, 0),
}
pm.Solaris = &specs.Solaris{}
return pm
}
func ContainerManifestFromImage(ref reference.Reference, img *image.Image) (pm *specs.Spec, err error) {
func ContainerManifestFromImage(ref reference.Reference, img *image.Image) (*ContainerManifest, error) {
manifest, ok := img.Tags[ref.Tag]
if !ok {
return nil, fmt.Errorf("manifest is null")
}
config, err := img.GetConfig(manifest.Config.Digest)
imageSpec, err := img.GetConfig(manifest.Config.Digest)
if err != nil {
return nil, tracerr.Wrap(err)
}
pm = EmptyContainerManifest()
m := EmptyContainerManifest()
for key, value := range manifest.Annotations {
if key == image.AnnotationMilestone {
pm.Solaris.Milestone = value
m.Spec.Solaris.Milestone = value
} else if key == image.AnnotationMinimumMemory {
pm.Solaris.CappedMemory = &specs.SolarisCappedMemory{
m.Spec.Solaris.CappedMemory = &specs.SolarisCappedMemory{
Physical: value,
Swap: value,
}
} else if key == image.AnnotationMinimumCPU {
pm.Solaris.CappedCPU = &specs.SolarisCappedCPU{
m.Spec.Solaris.CappedCPU = &specs.SolarisCappedCPU{
Ncpus: value,
}
} else if strings.HasPrefix(key, image.AnnotationPriviledge) {
if pm.Solaris.LimitPriv == "" {
pm.Solaris.LimitPriv = value
if m.Spec.Solaris.LimitPriv == "" {
m.Spec.Solaris.LimitPriv = value
} else {
pm.Solaris.LimitPriv += "," + value
m.Spec.Solaris.LimitPriv += "," + value
}
}
if strings.HasPrefix(key, volume.MetadataKeyPrefix) {
d := digest.Digest(value)
blob, err := img.GetBlob(d)
if err != nil {
return nil, fmt.Errorf("could not load volume configuration for digest %s from disk: %w", d, err)
}
var vol volume.Config
if err := json.Unmarshal(blob, &vol); err != nil {
return nil, fmt.Errorf("could not marshal volume configuration for digest %s: %w", d, err)
}
m.Volumes = append(m.Volumes, vol)
}
}
if len(config.Config.Volumes) > 0 {