diff --git a/.travis.yml b/.travis.yml index 17e670e..c52324e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,10 @@ os: go: - 1.9 +before_install: + - "sudo apt-get -qq update" + - "sudo apt-get install -y squashfs-tools" + install: - "mkdir -p $GOPATH/github.com/lxc" - "rsync -az ${TRAVIS_BUILD_DIR}/ $HOME/gopath/src/github.com/lxc/distrobuilder/" diff --git a/image/lxc.go b/image/lxc.go index 449eba4..10b90f3 100644 --- a/image/lxc.go +++ b/image/lxc.go @@ -7,6 +7,7 @@ import ( "strings" "time" + lxd "github.com/lxc/lxd/shared" pongo2 "gopkg.in/flosch/pongo2.v3" "github.com/lxc/distrobuilder/shared" @@ -102,15 +103,24 @@ func (l *LXCImage) createMetadata() error { var excludesUser string - filepath.Walk(filepath.Join(l.cacheDir, "rootfs", "dev"), - func(path string, info os.FileInfo, err error) error { - if info.Mode()&os.ModeDevice != 0 { - excludesUser += fmt.Sprintf("%s\n", - strings.TrimPrefix(path, filepath.Join(l.cacheDir, "rootfs"))) - } + if lxd.PathExists(filepath.Join(l.cacheDir, "rootfs", "dev")) { + err := filepath.Walk(filepath.Join(l.cacheDir, "rootfs", "dev"), + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } - return nil - }) + if info.Mode()&os.ModeDevice != 0 { + excludesUser += fmt.Sprintf("%s\n", + strings.TrimPrefix(path, filepath.Join(l.cacheDir, "rootfs"))) + } + + return nil + }) + if err != nil { + return fmt.Errorf("Error while walking /dev: %s", err) + } + } err = l.writeMetadata(filepath.Join(metaDir, "excludes-user"), excludesUser) if err != nil { @@ -121,8 +131,14 @@ func (l *LXCImage) createMetadata() error { } func (l *LXCImage) packMetadata() error { - err := shared.Pack("meta.tar.xz", filepath.Join(l.cacheDir, "metadata"), "config", - "config-user", "create-message", "expiry", "templates", "excludes-user") + files := []string{"config", "config-user", "create-message", "expiry", + "excludes-user"} + + if lxd.PathExists(filepath.Join(l.cacheDir, "metadata", "templates")) { + files = append(files, "templates") + } + + err := shared.Pack("meta.tar.xz", filepath.Join(l.cacheDir, "metadata"), files...) if err != nil { return fmt.Errorf("Failed to create metadata: %s", err) } diff --git a/image/lxc_test.go b/image/lxc_test.go new file mode 100644 index 0000000..803d73b --- /dev/null +++ b/image/lxc_test.go @@ -0,0 +1,257 @@ +package image + +import ( + "bytes" + "io" + "log" + "os" + "path/filepath" + "reflect" + "regexp" + "syscall" + "testing" + + "github.com/lxc/distrobuilder/shared" +) + +var lxcImageDef = shared.DefinitionImage{ + Description: "{{ image. Distribution|capfirst }} {{ image.Release }}", + Distribution: "ubuntu", + Release: "17.10", + Arch: "amd64", + Expiry: "30d", + Name: "{{ image.Distribution|lower }}-{{ image.Release }}-{{ image.Arch }}-{{ creation_date }}", +} + +var lxcTarget = shared.DefinitionTargetLXC{ + CreateMessage: "Welcome to {{ image.Distribution|capfirst}} {{ image.Release }}", + Config: `lxc.include = LXC_TEMPLATE_CONFIG/ubuntu.common.conf +lxc.arch = x86_64`, + ConfigUser: `lxc.include = LXC_TEMPLATE_CONFIG/ubuntu.common.conf +lxc.include = LXC_TEMPLATE_CONFIG/ubuntu.userns.conf +lxc.arch = x86_64`, +} + +func lxcCacheDir() string { + wd, _ := os.Getwd() + return filepath.Join(wd, "distrobuilder-test") +} + +func setupLXC() *LXCImage { + return NewLXCImage(lxcCacheDir(), lxcImageDef, lxcTarget) +} + +func teardownLXC() { + os.RemoveAll(lxcCacheDir()) +} + +func TestNewLXCImage(t *testing.T) { + image := NewLXCImage(lxcCacheDir(), lxcImageDef, lxcTarget) + defer teardownLXC() + + if image.cacheDir != lxcCacheDir() { + t.Fatalf("Expected image.cacheDir to be '%s', got '%s'", lxcCacheDir(), + image.cacheDir) + } + + if !reflect.DeepEqual(image.definition, lxcImageDef) { + t.Fatalf("lxcImageDef and image.definition are not equal") + } + + if !reflect.DeepEqual(image.target, lxcTarget) { + t.Fatalf("lxcTarget and image.target are not equal") + } +} + +func TestLXCAddTemplate(t *testing.T) { + image := setupLXC() + defer teardownLXC() + + // Make sure templates file is empty. + info, err := os.Stat(filepath.Join(lxcCacheDir(), "metadata", "templates")) + if err == nil && info.Size() > 0 { + t.Fatalf("Expected file size to be 0, got %d", info.Size()) + } + + // Add first template entry. + image.AddTemplate("/path/file1") + file, err := os.Open(filepath.Join(lxcCacheDir(), "metadata", "templates")) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + // Copy file content to buffer. + var buffer bytes.Buffer + io.Copy(&buffer, file) + file.Close() + + if buffer.String() != "/path/file1\n" { + t.Fatalf("Expected templates content to be '%s', got '%s'", + "/path/file", buffer.String()) + } + + // Add second template entry. + image.AddTemplate("/path/file2") + file, err = os.Open(filepath.Join(lxcCacheDir(), "metadata", "templates")) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + // Copy file content to buffer. + buffer.Reset() + io.Copy(&buffer, file) + file.Close() + + if buffer.String() != "/path/file1\n/path/file2\n" { + t.Fatalf("Expected templates content to be '%s', got '%s'", + "/path/file1\n/path/file2", buffer.String()) + } +} + +func TestLXCBuild(t *testing.T) { + image := setupLXC() + defer teardownLXC() + + err := os.MkdirAll(filepath.Join(lxcCacheDir(), "rootfs"), 0755) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + err = image.Build() + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + defer func() { + os.Remove("meta.tar.xz") + os.Remove("rootfs.tar.xz") + }() +} + +func TestLXCCreateMetadata(t *testing.T) { + defaultImage := setupLXC() + defer teardownLXC() + + tests := []struct { + name string + shouldFail bool + expectedError string + prepareImage func(LXCImage) *LXCImage + }{ + { + "valid metadata", + false, + "", + func(l LXCImage) *LXCImage { return &l }, + }, + { + "invalid config template", + true, + "Error writing 'config': .+", + func(l LXCImage) *LXCImage { + l.target.Config = "{{ invalid }" + return &l + }, + }, + { + "invalid config-user template", + true, + "Error writing 'config-user': .+", + func(l LXCImage) *LXCImage { + l.target.ConfigUser = "{{ invalid }" + return &l + }, + }, + { + "invalid create-message template", + true, + "Error writing 'create-message': .+", + func(l LXCImage) *LXCImage { + l.target.CreateMessage = "{{ invalid }" + return &l + }, + }, + { + "existing dev directory", + false, + "", + func(l LXCImage) *LXCImage { + // Create /dev and device file. + os.MkdirAll(filepath.Join(lxcCacheDir(), "rootfs", "dev"), 0755) + syscall.Mknod(filepath.Join(lxcCacheDir(), "rootfs", "dev", "null"), + syscall.S_IFCHR, 0) + return &l + }, + }, + } + + for i, tt := range tests { + log.Printf("Running test #%d: %s", i, tt.name) + image := tt.prepareImage(*defaultImage) + err := image.createMetadata() + if tt.shouldFail { + if err == nil { + t.Fatal("Expected to fail, but didn't") + } + + match, _ := regexp.MatchString(tt.expectedError, err.Error()) + if !match { + t.Fatalf("Expected to fail with '%s', got '%s'", tt.expectedError, + err.Error()) + } + } + if !tt.shouldFail && err != nil { + t.Fatalf("Unexpected error: %s", err) + } + } +} + +func TestLXCPackMetadata(t *testing.T) { + image := setupLXC() + defer func() { + teardownLXC() + os.Remove("meta.tar.xz") + }() + + err := image.createMetadata() + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + err = image.packMetadata() + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + // Include templates directory. + image.AddTemplate("/path/file") + err = image.packMetadata() + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + // Provoke error by removing the metadata directory + os.RemoveAll(filepath.Join(lxcCacheDir(), "metadata")) + err = image.packMetadata() + if err == nil { + t.Fatal("Expected failure") + } + +} + +func TestLXCWriteMetadata(t *testing.T) { + image := setupLXC() + defer teardownLXC() + + // Should fail due to invalid path + err := image.writeMetadata("/path/file", "") + if err == nil { + t.Fatal("Expected failure") + } + + // Should succeed + err = image.writeMetadata("test", "metadata") + if err != nil { + t.Fatalf("Unexpected failure: %s", err) + } + os.Remove("test") +} diff --git a/image/lxd.go b/image/lxd.go index ae3607c..f04b6c6 100644 --- a/image/lxd.go +++ b/image/lxd.go @@ -57,33 +57,44 @@ func (l *LXDImage) Build(unified bool) error { return fmt.Errorf("Failed to write metadata: %s", err) } - if unified { - var fname string - paths := []string{"rootfs", "templates", "metadata.yaml"} + paths := []string{"metadata.yaml"} + // Only include templates directory in the tarball if it's present. + info, err := os.Stat(filepath.Join(l.cacheDir, "templates")) + if err == nil && info.IsDir() { + paths = append(paths, "templates") + } + + if unified { ctx := pongo2.Context{ "image": l.definition, "creation_date": l.creationDate.Format("20060201_1504"), } + var fname string if l.definition.Name != "" { + // Use a custom name for the unified tarball. fname, _ = renderTemplate(l.definition.Name, ctx) } else { + // Default name for the unified tarball. fname = "lxd" } + paths = append(paths, "rootfs") err = shared.Pack(fmt.Sprintf("%s.tar.xz", fname), l.cacheDir, paths...) if err != nil { return err } } else { + // Create rootfs as squashfs. err = shared.RunCommand("mksquashfs", filepath.Join(l.cacheDir, "rootfs"), "rootfs.squashfs", "-noappend") if err != nil { return err } - err = shared.Pack("lxd.tar.xz", l.cacheDir, "templates", "metadata.yaml") + // Create metadata tarball. + err = shared.Pack("lxd.tar.xz", l.cacheDir, paths...) if err != nil { return err } @@ -107,9 +118,12 @@ func (l *LXDImage) createMetadata() error { return err } - l.Metadata.Architecture = arch + // Use proper architecture name from now on. + l.definition.Arch = arch + + l.Metadata.Architecture = l.definition.Arch l.Metadata.CreationDate = l.creationDate.Unix() - l.Metadata.Properties["architecture"] = arch + l.Metadata.Properties["architecture"] = l.definition.Arch l.Metadata.Properties["os"] = l.definition.Distribution l.Metadata.Properties["release"] = l.definition.Release diff --git a/image/lxd_test.go b/image/lxd_test.go new file mode 100644 index 0000000..48b2fad --- /dev/null +++ b/image/lxd_test.go @@ -0,0 +1,179 @@ +package image + +import ( + "fmt" + "log" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" + + "github.com/lxc/distrobuilder/shared" + lxd "github.com/lxc/lxd/shared" +) + +var lxdImageDef = shared.DefinitionImage{ + Description: "{{ image. Distribution|capfirst }} {{ image.Release }}", + Distribution: "ubuntu", + Release: "17.10", + Arch: "amd64", + Expiry: "30d", + Name: "{{ image.Distribution|lower }}-{{ image.Release }}-{{ image.Arch }}-{{ creation_date }}", +} + +func setupLXD(t *testing.T) *LXDImage { + cacheDir := filepath.Join(os.TempDir(), "distrobuilder-test") + + err := os.MkdirAll(filepath.Join(cacheDir, "rootfs"), 0755) + if err != nil { + t.Fatalf("Failed to create rootfs directory: %s", err) + } + + err = os.MkdirAll(filepath.Join(cacheDir, "templates"), 0755) + if err != nil { + t.Fatalf("Failed to create templates directory: %s", err) + } + + image := NewLXDImage(cacheDir, lxdImageDef) + + // Override creation date + image.creationDate = time.Date(2006, 1, 2, 3, 4, 5, 0, time.UTC) + + // Check cache directory + if image.cacheDir != cacheDir { + teardownLXD(t) + t.Fatalf("Expected cacheDir to be '%s', is '%s'", cacheDir, image.cacheDir) + } + + if !reflect.DeepEqual(lxdImageDef, image.definition) { + teardownLXD(t) + t.Fatal("lxdImageDef and image.definition are not equal") + } + + return image +} + +func teardownLXD(t *testing.T) { + os.RemoveAll(filepath.Join(os.TempDir(), "distrobuilder-test")) +} + +func TestLXDBuild(t *testing.T) { + image := setupLXD(t) + defer teardownLXD(t) + + testLXDBuildSplitImage(t, image) + testLXDBuildUnifiedImage(t, image) +} + +func testLXDBuildSplitImage(t *testing.T, image *LXDImage) { + // Create split tarball and squashfs. + err := image.Build(false) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + defer func() { + os.Remove("lxd.tar.xz") + os.Remove("rootfs.squashfs") + }() + + if !lxd.PathExists("lxd.tar.xz") { + t.Fatalf("File '%s' does not exist", "lxd.tar.xz") + } + + if !lxd.PathExists("rootfs.squashfs") { + t.Fatalf("File '%s' does not exist", "rootfs.squashfs") + } +} + +func testLXDBuildUnifiedImage(t *testing.T, image *LXDImage) { + // Create unified tarball with custom name. + err := image.Build(true) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + defer os.Remove("ubuntu-17.10-x86_64-20060201_0304.tar.xz") + + if !lxd.PathExists("ubuntu-17.10-x86_64-20060201_0304.tar.xz") { + t.Fatalf("File '%s' does not exist", "ubuntu-17.10-x86_64-20060201_0304.tar.xz") + } + + // Create unified tarball with default name. + image.definition.Name = "" + err = image.Build(true) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + defer os.Remove("lxd.tar.xz") + + if !lxd.PathExists("lxd.tar.xz") { + t.Fatalf("File '%s' does not exist", "lxd.tar.xz") + } +} + +func TestLXDCreateMetadata(t *testing.T) { + image := setupLXD(t) + defer teardownLXD(t) + + err := image.createMetadata() + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + tests := []struct { + name string + have string + expected string + }{ + { + "Architecture", + image.Metadata.Architecture, + "x86_64", + }, + { + "CreationDate", + string(image.Metadata.CreationDate), + string(image.creationDate.Unix()), + }, + { + "Properties[architecture]", + image.Metadata.Properties["architecture"], + "x86_64", + }, + { + "Properties[os]", + image.Metadata.Properties["os"], + lxdImageDef.Distribution, + }, + { + "Properties[release]", + image.Metadata.Properties["release"], + lxdImageDef.Release, + }, + { + "Properties[description]", + image.Metadata.Properties["description"], + fmt.Sprintf("%s %s", strings.Title(lxdImageDef.Distribution), + lxdImageDef.Release), + }, + { + "Properties[name]", + image.Metadata.Properties["name"], + fmt.Sprintf("%s-%s-%s-%s", strings.ToLower(lxdImageDef.Distribution), + lxdImageDef.Release, "x86_64", image.creationDate.Format("20060201_1504")), + }, + { + "ExpiryDate", + fmt.Sprintf("%d", image.Metadata.ExpiryDate), + "1138763045", + }, + } + + for i, tt := range tests { + log.Printf("Running test #%d: %s", i, tt.name) + if tt.have != tt.expected { + t.Fatalf("Expected '%s', got '%s'", tt.expected, tt.have) + } + } +}