Adds both build and push support

This adds support for building and pushing docker containers
when you perform either `kompose convert` or `kompose up`.

Docker Compose files who have build parameters with their respective
image and build keys will automatically be both built and pushed.
This commit is contained in:
Charlie Drage
2017-06-14 10:19:12 -04:00
parent 49ada134ae
commit e2f9084003
26 changed files with 1664 additions and 555 deletions
+38 -14
View File
@@ -64,15 +64,6 @@ const TIMEOUT = 300
//default size of Persistent Volume Claim
const PVCRequestSize = "100Mi"
// list of all unsupported keys for this transformer
// Keys are names of variables in kobject struct.
// this is map to make searching for keys easier
// to make sure that unsupported key is not going to be reported twice
// by keeping record if already saw this key in another service
var unsupportedKey = map[string]bool{
"Build": false,
}
// CheckUnsupportedKey checks if given komposeObject contains
// keys that are not supported by this tranfomer.
// list of all unsupported keys are stored in unsupportedKey variable
@@ -532,11 +523,6 @@ func (k *Kubernetes) InitPod(name string, service kobject.ServiceConfig) *api.Po
// returns object that are already sorted in the way that Services are first
func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject.ConvertOptions) ([]runtime.Object, error) {
noSupKeys := k.CheckUnsupportedKey(&komposeObject, unsupportedKey)
for _, keyName := range noSupKeys {
log.Warningf("Kubernetes provider doesn't support %s key - ignoring", keyName)
}
// this will hold all the converted data
var allobjects []runtime.Object
@@ -545,6 +531,44 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject.
service := komposeObject.ServiceConfigs[name]
var objects []runtime.Object
// Must build the images before conversion (got to add service.Image in case 'image' key isn't provided
// Check that --build is set to true
// Check to see if there is an InputFile (required!) before we build the container
// Check that there's actually a Build key
// Lastly, we must have an Image name to continue
if opt.Build == "local" && opt.InputFiles != nil && service.Build != "" {
if service.Image == "" {
return nil, fmt.Errorf("image key required within build parameters in order to build and push service '%s'", name)
}
log.Infof("Build key detected. Attempting to build and push image '%s'", service.Image)
// Get the directory where the compose file is
composeFileDir, err := transformer.GetComposeFileDir(opt.InputFiles)
if err != nil {
return nil, err
}
// Build the container!
err = transformer.BuildDockerImage(service, name, composeFileDir)
if err != nil {
return nil, errors.Wrapf(err, "Unable to build Docker image for service %v", name)
}
// Push the built container to the repo!
err = transformer.PushDockerImage(service, name)
if err != nil {
return nil, errors.Wrapf(err, "Unable to push Docker image for service %v", name)
}
}
// If there's no "image" key, use the name of the container that's built
if service.Image == "" {
service.Image = name
}
// Generate pod only and nothing more
if service.Restart == "no" || service.Restart == "on-failure" {
// Error out if Controller Object is specified with restart: 'on-failure'
@@ -19,7 +19,6 @@ package kubernetes
import (
"fmt"
"reflect"
"strings"
"testing"
deployapi "github.com/openshift/origin/pkg/deploy/api"
@@ -428,39 +427,6 @@ func TestConvertRestartOptions(t *testing.T) {
}
}
// TestUnsupportedKeys test checkUnsupportedKey function
func TestUnsupportedKeys(t *testing.T) {
kobjectWithBuild := newKomposeObject()
kobjectWithBuild.LoadedFrom = "compose"
serviceConfig := kobjectWithBuild.ServiceConfigs["app"]
serviceConfig.Build = "./asdf"
serviceConfig.Network = []string{}
kobjectWithBuild.ServiceConfigs = map[string]kobject.ServiceConfig{"app": serviceConfig}
// define all test cases for checkUnsupportedKey function
testCases := map[string]struct {
bundleFile kobject.KomposeObject
expectedUnsupportedKeys []string
}{
"Full Bundle": {
kobjectWithBuild,
[]string{"build"},
},
}
k := Kubernetes{}
for name, test := range testCases {
t.Log("Test case:", name)
keys := k.CheckUnsupportedKey(&test.bundleFile, unsupportedKey)
if !reflect.DeepEqual(keys, test.expectedUnsupportedKeys) {
t.Errorf("ERROR: Expecting unsupported keys: ['%s']. Got: ['%s']", strings.Join(test.expectedUnsupportedKeys, "', '"), strings.Join(keys, "', '"))
}
}
}
func TestRestartOnFailure(t *testing.T) {
kobjectWithRestartOnFailure := newKomposeObject()
+105 -51
View File
@@ -20,7 +20,6 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/kubernetes-incubator/kompose/pkg/kobject"
@@ -100,6 +99,27 @@ func getImageTag(image string) string {
}
// Inputs name of the service + provided image
// Outputs name is image is blank, but still provide a tag.
func taggedImage(name string, serviceImage string) string {
var image string
tag := getImageTag(serviceImage)
// Use the image name if not blank
if serviceImage != "" {
image = serviceImage
} else {
image = name
}
// Add a tag if not present
if !strings.Contains(image, ":") {
image = image + ":" + tag
}
return image
}
// hasGitBinary checks if the 'git' binary is available on the system
func hasGitBinary() bool {
_, err := exec.LookPath("git")
@@ -134,20 +154,6 @@ func getGitCurrentBranch(composeFileDir string) (string, error) {
return strings.TrimRight(string(out), "\n"), nil
}
// getComposeFileDir returns compose file directory
func getComposeFileDir(inputFiles []string) (string, error) {
// Lets assume all the docker-compose files are in the same directory
inputFile := inputFiles[0]
if strings.Index(inputFile, "/") != 0 {
workDir, err := os.Getwd()
if err != nil {
return "", err
}
inputFile = filepath.Join(workDir, inputFile)
}
return filepath.Dir(inputFile), nil
}
// getAbsBuildContext returns build context relative to project root dir
func getAbsBuildContext(context string) (string, error) {
cmd := exec.Command("git", "rev-parse", "--show-prefix")
@@ -163,6 +169,8 @@ func getAbsBuildContext(context string) (string, error) {
// initImageStream initialize ImageStream object
func (o *OpenShift) initImageStream(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) *imageapi.ImageStream {
// Retrieve tags and image name for mapping
tag := getImageTag(service.Image)
var importPolicy imageapi.TagImportPolicy
@@ -171,7 +179,8 @@ func (o *OpenShift) initImageStream(name string, service kobject.ServiceConfig,
}
var tags map[string]imageapi.TagReference
if service.Build == "" {
if service.Build != "" || opt.Build != "build-config" {
tags = map[string]imageapi.TagReference{
tag: imageapi.TagReference{
From: &kapi.ObjectReference{
@@ -199,7 +208,6 @@ func (o *OpenShift) initImageStream(name string, service kobject.ServiceConfig,
return is
}
// initBuildConfig initialize Openshifts BuildConfig Object
func initBuildConfig(name string, service kobject.ServiceConfig, repo string, branch string) (*buildapi.BuildConfig, error) {
contextDir, err := getAbsBuildContext(service.Build)
envList := transformer.EnvSort{}
@@ -230,7 +238,6 @@ func initBuildConfig(name string, service kobject.ServiceConfig, repo string, br
Spec: buildapi.BuildConfigSpec{
Triggers: []buildapi.BuildTriggerPolicy{
{Type: "ConfigChange"},
{Type: "ImageChange"},
},
RunPolicy: "Serial",
CommonSpec: buildapi.CommonSpec{
@@ -261,9 +268,11 @@ func initBuildConfig(name string, service kobject.ServiceConfig, repo string, br
// initDeploymentConfig initialize OpenShifts DeploymentConfig object
func (o *OpenShift) initDeploymentConfig(name string, service kobject.ServiceConfig, replicas int) *deployapi.DeploymentConfig {
tag := getImageTag(service.Image)
containerName := []string{name}
// Properly add tags to the image name
tag := getImageTag(service.Image)
// Use ContainerName if it was set
if service.ContainerName != "" {
containerName = []string{service.ContainerName}
@@ -351,7 +360,6 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C
var allobjects []runtime.Object
var err error
var composeFileDir string
hasBuild := false
buildRepo := opt.BuildRepo
buildBranch := opt.BuildBranch
@@ -360,6 +368,41 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C
service := komposeObject.ServiceConfigs[name]
var objects []runtime.Object
// Must build the images before conversion (got to add service.Image in case 'image' key isn't provided
// Check to see if there is an InputFile (required!) before we build the container
// Check that there's actually a Build key
// Lastly, we must have an Image name to continue
if opt.Build == "local" && opt.InputFiles != nil && service.Build != "" {
if service.Image == "" {
return nil, fmt.Errorf("image key required within build parameters in order to build and push service '%s'", name)
}
// Get the directory where the compose file is
composeFileDir, err := transformer.GetComposeFileDir(opt.InputFiles)
if err != nil {
return nil, err
}
// Build the container!
err = transformer.BuildDockerImage(service, name, composeFileDir)
if err != nil {
log.Fatalf("Unable to build Docker container for service %v: %v", name, err)
}
// Push the built container to the repo!
err = transformer.PushDockerImage(service, name)
if err != nil {
log.Fatalf("Unable to push Docker image for service %v: %v", name, err)
}
}
// If there's no "image" key, use the name of the container that's built
if service.Image == "" {
service.Image = name
}
// Generate pod only and nothing more
if service.Restart == "no" || service.Restart == "on-failure" {
// Error out if Controller Object is specified with restart: 'on-failure'
@@ -378,38 +421,49 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C
}
// buildconfig needs to be added to objects after imagestream because of this Openshift bug: https://github.com/openshift/origin/issues/4518
if service.Build != "" {
if !hasBuild {
composeFileDir, err = getComposeFileDir(opt.InputFiles)
if err != nil {
log.Warningf("Error in detecting compose file's directory.")
continue
}
if !hasGitBinary() && (buildRepo == "" || buildBranch == "") {
return nil, errors.New("Git is not installed! Please install Git to create buildconfig, else supply source repository and branch to use for build using '--build-repo', '--build-branch' options respectively")
}
if buildBranch == "" {
buildBranch, err = getGitCurrentBranch(composeFileDir)
if err != nil {
return nil, errors.Wrap(err, "Buildconfig cannot be created because current git branch couldn't be detected.")
}
}
if opt.BuildRepo == "" {
if err != nil {
return nil, errors.Wrap(err, "Buildconfig cannot be created because remote for current git branch couldn't be detected.")
}
buildRepo, err = getGitCurrentRemoteURL(composeFileDir)
if err != nil {
return nil, errors.Wrap(err, "Buildconfig cannot be created because git remote origin repo couldn't be detected.")
}
}
hasBuild = true
// Generate BuildConfig if the parameter has been passed
if service.Build != "" && opt.Build == "build-config" {
// Get the compose file directory
composeFileDir, err = transformer.GetComposeFileDir(opt.InputFiles)
if err != nil {
log.Warningf("Error in detecting compose file's directory.")
continue
}
// Check for Git
if !hasGitBinary() && (buildRepo == "" || buildBranch == "") {
return nil, errors.New("Git is not installed! Please install Git to create buildconfig, else supply source repository and branch to use for build using '--build-repo', '--build-branch' options respectively")
}
// Check the Git branch
if buildBranch == "" {
buildBranch, err = getGitCurrentBranch(composeFileDir)
if err != nil {
return nil, errors.Wrap(err, "Buildconfig cannot be created because current git branch couldn't be detected.")
}
}
// Detect the remote branches
if opt.BuildRepo == "" {
if err != nil {
return nil, errors.Wrap(err, "Buildconfig cannot be created because remote for current git branch couldn't be detected.")
}
buildRepo, err = getGitCurrentRemoteURL(composeFileDir)
if err != nil {
return nil, errors.Wrap(err, "Buildconfig cannot be created because git remote origin repo couldn't be detected.")
}
}
// Initialize and build BuildConfig
bc, err := initBuildConfig(name, service, buildRepo, buildBranch)
if err != nil {
return nil, errors.Wrap(err, "initBuildConfig failed")
}
objects = append(objects, bc) // Openshift BuildConfigs
// Log what we're doing
log.Infof("Buildconfig using %s::%s as source.", buildRepo, buildBranch)
}
// If ports not provided in configuration we will not make service
@@ -425,18 +479,18 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C
objects = append(objects, svc)
}
}
o.UpdateKubernetesObjects(name, service, &objects)
// Update and then append the objects (we're done generating)
o.UpdateKubernetesObjects(name, service, &objects)
allobjects = append(allobjects, objects...)
}
if hasBuild {
log.Infof("Buildconfig using %s::%s as source.", buildRepo, buildBranch)
}
// If docker-compose has a volumes_from directive it will be handled here
o.VolumesFrom(&allobjects, komposeObject)
// sort all object so Services are first
// sort all object so all services are first
o.SortServicesFirst(&allobjects)
return allobjects, nil
}
+2 -1
View File
@@ -28,6 +28,7 @@ import (
"github.com/kubernetes-incubator/kompose/pkg/kobject"
"github.com/kubernetes-incubator/kompose/pkg/testutils"
"github.com/kubernetes-incubator/kompose/pkg/transformer"
"github.com/kubernetes-incubator/kompose/pkg/transformer/kubernetes"
"github.com/pkg/errors"
)
@@ -223,7 +224,7 @@ func TestGetComposeFileDir(t *testing.T) {
for name, test := range testCases {
t.Log("Test case: ", name)
output, err = getComposeFileDir(test.inputFiles)
output, err = transformer.GetComposeFileDir(test.inputFiles)
if err != nil {
t.Errorf("Expected success, got error: %#v", err)
+79
View File
@@ -20,11 +20,13 @@ import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"
log "github.com/Sirupsen/logrus"
"github.com/kubernetes-incubator/kompose/pkg/kobject"
"github.com/kubernetes-incubator/kompose/pkg/utils/docker"
"path/filepath"
"github.com/pkg/errors"
@@ -171,3 +173,80 @@ func (env EnvSort) Less(i, j int) bool {
func (env EnvSort) Swap(i, j int) {
env[i], env[j] = env[j], env[i]
}
// GetComposeFileDir returns compose file directory
func GetComposeFileDir(inputFiles []string) (string, error) {
// Lets assume all the docker-compose files are in the same directory
inputFile := inputFiles[0]
if strings.Index(inputFile, "/") != 0 {
workDir, err := os.Getwd()
if err != nil {
return "", err
}
inputFile = filepath.Join(workDir, inputFile)
}
log.Debugf("Compose file dir: %s", filepath.Dir(inputFile))
return filepath.Dir(inputFile), nil
}
func BuildDockerImage(service kobject.ServiceConfig, name string, relativePath string) error {
// First, let's figure out the relative path of the Dockerfile!
// else, we error out.
if _, err := os.Stat(service.Build); err != nil {
return errors.Wrapf(err, "%s is not a valid path for building image %s. Check if this dir exists.", service.Build, name)
}
// Get the appropriate image source and name
// use path.Base to get the last element of the relative build path
imagePath := path.Join(relativePath, path.Base(service.Build))
imageName := name
if service.Image != "" {
imageName = service.Image
}
// Connect to the Docker client
client, err := docker.DockerClient()
if err != nil {
return err
}
// Use the build struct function to build the image
// Build the image!
build := docker.Build{*client}
err = build.BuildImage(imagePath, imageName)
if err != nil {
return err
}
return nil
}
func PushDockerImage(service kobject.ServiceConfig, serviceName string) error {
log.Debugf("Pushing Docker image '%s'", service.Image)
// Don't do anything if service.Image is blank, but at least WARN about it
// lse, let's push the image
if service.Image == "" {
log.Warnf("No image name has been passed for service %s, skipping pushing to repository", serviceName)
return nil
} else {
// Connect to the Docker client
client, err := docker.DockerClient()
if err != nil {
return err
}
push := docker.Push{*client}
err = push.PushImage(service.Image)
if err != nil {
return err
}
}
return nil
}
+11
View File
@@ -18,6 +18,7 @@ package transformer
import (
"fmt"
"strings"
"testing"
)
@@ -138,3 +139,13 @@ func TestParseVolume(t *testing.T) {
}
}
}
func TestGetComposeFileDir(t *testing.T) {
output, err := GetComposeFileDir([]string{"foobar/docker-compose.yaml"})
if err != nil {
t.Errorf("Error with GetComposeFileDir %v", err)
}
if !strings.Contains(output, "foobar") {
t.Errorf("Expected $PWD/foobar, got %v", output)
}
}