diff --git a/cmd/build.go b/cmd/build.go index e00d6871c..9fa55577f 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -22,13 +22,14 @@ import ( "github.com/kedgeproject/kedge/pkg/build" + log "github.com/Sirupsen/logrus" "github.com/spf13/cobra" ) var Dockerfile string -var DockerImage string +var DockerImage, BuilderImage string var DockerContext string -var PushImage bool +var PushImage, s2iBuild bool var buildCmd = &cobra.Command{ Use: "build", @@ -38,9 +39,24 @@ var buildCmd = &cobra.Command{ fmt.Println("Please specify the container image name using flag '--image' or '-i'") os.Exit(-1) } - if err := build.BuildPushDockerImage(Dockerfile, DockerImage, DockerContext, PushImage); err != nil { - fmt.Println(err) - os.Exit(-1) + + if s2iBuild { + if PushImage { + log.Warnf("Using source to image strategy for build, image will be by default pushed to internal container registry, so ignoring this flag") + } + if BuilderImage == "" { + fmt.Println("Please specify the builder image name using flag '--builder-image' or '-b'") + os.Exit(-1) + } + if err := build.BuildS2I(DockerImage, DockerContext, BuilderImage); err != nil { + fmt.Println(err) + os.Exit(-1) + } + } else { + if err := build.BuildPushDockerImage(Dockerfile, DockerImage, DockerContext, PushImage); err != nil { + fmt.Println(err) + os.Exit(-1) + } } }, } @@ -50,7 +66,9 @@ func init() { buildCmd.Flags().StringVarP(&Dockerfile, "file", "f", "Dockerfile", "Specify Dockerfile for doing builds, Dockerfile path is relative to context") buildCmd.Flags().StringVarP(&DockerImage, "image", "i", "", "Image name and tag of resulting image") buildCmd.Flags().StringVarP(&DockerContext, "context", "c", ".", "Path to a directory containing a Dockerfile, it is build context that is sent to the Docker daemon") - buildCmd.Flags().BoolVarP(&PushImage, "push", "p", false, "Add this flag if you want to push the image") + buildCmd.Flags().BoolVarP(&PushImage, "push", "p", false, "Add this flag if you want to push the image. Note: Ignored when s2i build strategy used") + buildCmd.Flags().BoolVarP(&s2iBuild, "s2i", "", false, "If this is enabled then Source to Image build strategy is used") + buildCmd.Flags().StringVarP(&BuilderImage, "builder-image", "b", "", "Name of a Docker image to use as a builder. Note: This is only useful when using s2i build strategy") RootCmd.AddCommand(buildCmd) } diff --git a/docs/user-guide.md b/docs/user-guide.md index 46b73a537..557fc8abf 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -193,3 +193,22 @@ command before running this build command: ```console $ eval $(minikube docker-env) ``` + +### Build using source to image + +You can hand off the image building work to OpenShift's source to image utility, without having +to write any Dockerfile. + +To do build using OpensShift's s2i run following command: + +```console +$ kedge build --s2i --image pyappth -b centos/python-35-centos7:3.5 +``` + +Deconstructing above command, we can see that to enable using OpenShift's s2i use the boolean +flag `--s2i` then give the output name of the ImageStream that will be created using flag +`--image` and finally also provide what builder image to use to do builds of the code using +flag `-b`. + +**Note**: Flag `-b` is valid only when using s2i mode. And build flag `-p` is not valid when +in s2i mode. diff --git a/pkg/build/build.go b/pkg/build/build.go index 5c0f14414..87ecaf6fa 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -28,7 +28,16 @@ import ( log "github.com/Sirupsen/logrus" dockerlib "github.com/fsouza/go-dockerclient" + "github.com/ghodss/yaml" + os_build_v1 "github.com/openshift/origin/pkg/build/apis/build/v1" + os_image_v1 "github.com/openshift/origin/pkg/image/apis/image/v1" "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + _ "k8s.io/kubernetes/pkg/api/install" + kapi "k8s.io/kubernetes/pkg/api/v1" + + "github.com/kedgeproject/kedge/pkg/cmd" + "github.com/kedgeproject/kedge/pkg/spec" ) func BuildPushDockerImage(dockerfile, image, context string, push bool) error { @@ -174,3 +183,148 @@ func CreateTarball(source, target string) error { return err }) } + +// getImageTag get tag name from image name +// if no tag is specified return 'latest' +func GetImageTag(image string) string { + // format: registry_host:registry_port/repo_name/image_name:image_tag + // example: + // 1) myregistryhost:5000/fedora/httpd:version1.0 + // 2) myregistryhost:5000/fedora/httpd + // 3) myregistryhost/fedora/httpd:version1.0 + // 4) myregistryhost/fedora/httpd + // 5) fedora/httpd + // 6) httpd + imageAndTag := image + + imageTagSplit := strings.Split(image, "/") + if len(imageTagSplit) >= 2 { + imageAndTag = imageTagSplit[len(imageTagSplit)-1] + } + + p := strings.Split(imageAndTag, ":") + if len(p) == 2 { + return p[1] + } + return "latest" +} + +func GetImageName(image string) string { + imageAndTag := image + + imageTagSplit := strings.Split(image, "/") + if len(imageTagSplit) >= 2 { + imageAndTag = imageTagSplit[len(imageTagSplit)-1] + } + p := strings.Split(imageAndTag, ":") + if len(p) <= 2 { + return p[0] + } + + return image +} + +func BuildS2I(image, context, builderImage string) error { + + name := GetImageName(image) + labels := map[string]string{ + spec.BuildLabelKey: name, + } + annotations := map[string]string{ + "openshift.io/generated-by": "KedgeBuildS2I", + } + + is := os_image_v1.ImageStream{ + TypeMeta: metav1.TypeMeta{ + Kind: "ImageStream", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: labels, + Annotations: annotations, + }, + Spec: os_image_v1.ImageStreamSpec{ + Tags: []os_image_v1.TagReference{ + {Name: GetImageTag(image)}, + }, + }, + } + + bc := os_build_v1.BuildConfig{ + TypeMeta: metav1.TypeMeta{ + Kind: "BuildConfig", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: labels, + Annotations: annotations, + }, + Spec: os_build_v1.BuildConfigSpec{ + CommonSpec: os_build_v1.CommonSpec{ + Strategy: os_build_v1.BuildStrategy{ + Type: "Binary", + SourceStrategy: &os_build_v1.SourceBuildStrategy{ + From: kapi.ObjectReference{ + Kind: "DockerImage", + Name: builderImage, + }, + }, + }, + Output: os_build_v1.BuildOutput{ + To: &kapi.ObjectReference{ + Kind: "ImageStreamTag", + Name: name + ":" + GetImageTag(image), + }, + }, + }, + }, + } + + isyaml, err := yaml.Marshal(is) + if err != nil { + return err + } + + bcyaml, err := yaml.Marshal(bc) + if err != nil { + return err + } + + log.Debugf("ImageStream for output image: \n%s\n", string(isyaml)) + log.Debugf("BuildConfig: \n%s\n", string(bcyaml)) + + args := []string{"apply", "-f", "-"} + err = cmd.RunClusterCommand(args, isyaml, true) + if err != nil { + return err + } + err = cmd.RunClusterCommand(args, bcyaml, true) + if err != nil { + cleanup(name) + return err + } + + log.Infof("Starting build for %q", image) + cmd := []string{"oc", "start-build", image, "--from-dir=" + context, "-F"} + if err := RunCommand(cmd); err != nil { + return err + } + + return nil +} + +func cleanup(name string) { + log.Infof("Cleaning up build since error occurred while building") + + delBc := []string{"oc", "delete", "buildconfig", name} + if err := RunCommand(delBc); err != nil { + log.Debugf("error while deleting buildconfig: %v", err) + } + + delIs := []string{"oc", "delete", "imagestream", name} + if err := RunCommand(delIs); err != nil { + log.Debugf("error while deleting imagestream: %v", err) + } +} diff --git a/pkg/build/push.go b/pkg/build/push.go index a2d5c1f38..3cff62030 100644 --- a/pkg/build/push.go +++ b/pkg/build/push.go @@ -33,7 +33,17 @@ as input. func PushImage(fullImageName string) error { log.Infof("Pushing image %q", fullImageName) - cmd := exec.Command("docker", "push", fullImageName) + command := []string{"docker", "push", fullImageName} + if err := RunCommand(command); err != nil { + return err + } + log.Infof("Successfully pushed image %q", fullImageName) + + return nil +} + +func RunCommand(command []string) error { + cmd := exec.Command(command[0], command[1:]...) cmdReader, err := cmd.StdoutPipe() if err != nil { return err @@ -58,7 +68,6 @@ func PushImage(fullImageName string) error { if err != nil { return fmt.Errorf("%s, %s", strings.TrimSpace(stderr.String()), err) } - log.Infof("Successfully pushed image %q", fullImageName) return nil } diff --git a/pkg/cmd/commands.go b/pkg/cmd/commands.go index 618a268b0..b33968d81 100644 --- a/pkg/cmd/commands.go +++ b/pkg/cmd/commands.go @@ -90,7 +90,7 @@ func CreateArtifacts(paths []string, generate bool, args ...string) error { // e.g. If the command and arguments are "apply --namespace staging", then the // final command becomes "kubectl apply --namespace staging -f -" arguments := append(args, "-f", "-") - err = runClusterCommand(arguments, data, useOC) + err = RunClusterCommand(arguments, data, useOC) if err != nil { return errors.Wrap(err, "failed to execute command") } @@ -118,7 +118,7 @@ func CreateArtifacts(paths []string, generate bool, args ...string) error { // e.g. If the command and arguments are "apply --namespace staging", then the // final command becomes "kubectl apply --namespace staging -f absolute-filename" arguments := append(args, "-f", file) - err = runClusterCommand(arguments, nil, useOC) + err = RunClusterCommand(arguments, nil, useOC) if err != nil { return errors.Wrap(err, "failed to execute command") } @@ -130,7 +130,7 @@ func CreateArtifacts(paths []string, generate bool, args ...string) error { // runClusterCommand calls kubectl or oc binary. // Boolean flag useOC controls if oc or kubectl will be used -func runClusterCommand(args []string, data []byte, useOC bool) error { +func RunClusterCommand(args []string, data []byte, useOC bool) error { executable := "kubectl" if useOC { executable = "oc" diff --git a/pkg/spec/resources.go b/pkg/spec/resources.go index 46c948116..c4a0b119e 100644 --- a/pkg/spec/resources.go +++ b/pkg/spec/resources.go @@ -44,8 +44,11 @@ import ( // allLabelKey is the key that Kedge injects in every Kubernetes resource that // it generates as an ObjectMeta label -const appLabelKey = "app" -const appVersion = "appversion" +const ( + appLabelKey = "app" + appVersion = "appversion" + BuildLabelKey = "build" +) // Fix