Files
mixer-tools/builder/docker.go
T
Tudor Marcu 201110598b Fix order of format bump builds and bug fixes
This patch includes the following changes:

Reorder which version builds first to simplify

The +20 build can be built fully at the beginning all the way though,
removing the need to swap tooling to build the +10 (just so things are
built in chronological order). The +10 build is created with the -
possibly - old tooling only after, and reduces the amount of
back-and-forth swapping of tooling.

Fix misc errors with formats and version blocks

The right format number needs to be passed into both functions, and we
*must* ignore the fact that the version and format are lower than the
previous build in the format bump case. It reduces complexity and tool
changes by building the +20 version all the way through first.

Two top level commands now exist for running format bumps:
- build upstream-format This command builds the necessary builds to
			cross a mix over a format bump.
- build format-bump This command builds a bump for downstream users.

Fix docker mount path lookup

This patch fixes a bug with the way mixer determined which fields in
'builder.conf' were mountable paths.

Previously, mixer treated every field in [Builder] and [Mixer] as paths
on the filesystem (or paths to files whose parent directories needed to
be mounted). Introducing the "DOCKER_IMAGE_PATH" field broke this
approach.

This patch introduces a new "mount" tag on the config struct fields that
indicates whether a config field represents a mountable path. This has
several benefits:
1) It allows us to know definitively which fields we should look at.
2) It allows us to look at the entire 'builder.conf', not just the
[Builder] and [Mixer] sections.
3) It removes the restriction that paths in 'builder.conf' be absolute.
This absolute check was mostly a sloppy way of checking if a field was
in fact a path.

Signed-off-by: Tudor Marcu <tudor.marcu@intel.com>
Signed-off-by: Kevin C. Wells <kevin.c.wells@intel.com>
2018-06-07 17:13:47 -07:00

198 lines
5.6 KiB
Go

// Copyright © 2018 Intel Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package builder
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/clearlinux/mixer-tools/helpers"
)
// GetHostAndUpstreamFormats retreives the formats for the host and the mix's
// upstream version. It attempts to determine the format for the host machine,
// and if successful, looks up the format for the desired upstream version.
func (b *Builder) GetHostAndUpstreamFormats() (string, string, error) {
// Determine the host's format
hostFormat, err := ioutil.ReadFile("/usr/share/defaults/swupd/format")
if err != nil && !os.IsNotExist(err) {
return "", "", err
}
// Get the upstream format
upstreamFormat, err := b.DownloadFileFromUpstreamAsString(fmt.Sprintf("update/%s/format", b.UpstreamVer))
if err != nil {
return "", "", err
}
return string(hostFormat), upstreamFormat, nil
}
func (b *Builder) getDockerImageName(format string) string {
return fmt.Sprintf("%s:%s", b.Config.Mixer.DockerImgPath, format)
}
// reduceDockerMounts takes a list of directory paths and reduces it to a
// minimal, non-redundant list. For example, if the list includes both "/foo"
// and "/foo/bar", then "/foo/bar" would be removed, as its parent is already
// in the list. This function requires paths to have no trailing slash.
func reduceDockerMounts(paths []string) []string {
if len(paths) <= 1 {
return paths
}
sort.Strings(paths) // Puts "/foo" before "/foo/bar"
for i := 1; i < len(paths); i++ {
if paths[i] == paths[i-1] || strings.HasPrefix(paths[i], paths[i-1]+"/") { // "/" is to prevent "/foobar" matching "/foo"
paths = append(paths[:i], paths[i+1:]...)
i-- // Because removal shifts things left
}
}
return paths
}
// canAccess checks whether mixer has read/write access to the given directory.
// A nil error means success.
func canAccess(dir string) error {
// Check read access
if _, err := ioutil.ReadDir(dir); err != nil {
return err
}
// Check write acess
f, err := ioutil.TempFile(dir, "")
if err != nil {
return fmt.Errorf("open %s: permission denied", dir)
}
defer func() {
_ = os.Remove(f.Name())
_ = f.Close()
}()
return nil
}
// getPathDir returns the directory portion of a config file path. If the path
// is already to a directory, the path is returned unchanged. If it is instead
// to a file or does not exist, its parent is returned. This is needed because
// some values in the config are paths to files or directories that get created
// by the commands, but their parent directory exists and needs to be mounted.
func getDirFromConfigPath(path string) (string, error) {
f, err := os.Stat(path)
if os.IsNotExist(err) || (err == nil && !f.Mode().IsDir()) {
path = filepath.Dir(path)
f, err = os.Stat(path)
}
if err != nil {
return "", err
}
return path, nil
}
// addConfigFieldPaths loops through each field in a config section, verifying
// and adding its value to the mounts slice.
func addConfigFieldPaths(section reflect.Value, mounts *[]string) error {
sectionT := reflect.TypeOf(section.Interface())
for i := 0; i < section.NumField(); i++ {
// Check if the field is tagged as mountable and has a value
tag, ok := sectionT.Field(i).Tag.Lookup("mount")
if !ok || tag != "true" || section.Field(i).String() == "" {
continue
}
path, err := getDirFromConfigPath(section.Field(i).String())
if err != nil {
return err
}
if err := canAccess(path); err != nil {
return err
}
*mounts = append(*mounts, path)
}
return nil
}
// getDockerMounts returns a minimal list of all directories in the config that
// need to be mounted inside the container. Only the "Buiilder" and "Mixer"
// sections of the conf are parsed.
func (b *Builder) getDockerMounts() ([]string, error) {
wd, _ := os.Getwd()
mounts := []string{wd}
rc := reflect.ValueOf(b.Config)
for i := 0; i < rc.NumField(); i++ {
err := addConfigFieldPaths(rc.Field(i), &mounts)
if err != nil {
return nil, err
}
}
return reduceDockerMounts(mounts), nil
}
// RunCommandInContainer will pull the content necessary to build a docker
// image capable of running the desired command, build that image, and then
// run the command in that image.
func (b *Builder) RunCommandInContainer(cmd []string) error {
format, err := b.getUpstreamFormat(b.UpstreamVer)
if err != nil {
return err
}
fmt.Printf("Running command in container: %q\n", strings.Join(cmd, " "))
wd, _ := os.Getwd()
// Build Docker image
dockerCmd := []string{
"docker",
"run",
"-i",
"--network=host",
"--rm",
"--workdir", wd,
"--entrypoint", cmd[0],
}
mounts, err := b.getDockerMounts()
if err != nil {
return errors.Wrap(err, "Failed to extract mountable directories from config")
}
for _, path := range mounts {
dockerCmd = append(dockerCmd, "-v", fmt.Sprintf("%s:%s", path, path))
}
dockerCmd = append(dockerCmd, b.getDockerImageName(format))
dockerCmd = append(dockerCmd, cmd[1:]...)
dockerCmd = append(dockerCmd, "--native")
// Run command
if err := helpers.RunCommand(dockerCmd[0], dockerCmd[1:]...); err != nil {
return errors.Wrap(err, "Failed to run command in container")
}
return nil
}