From 09e521fc4b0cf3b3128054191bef1f4a748124d2 Mon Sep 17 00:00:00 2001 From: Suraj Deshmukh Date: Wed, 15 Nov 2017 16:00:34 +0530 Subject: [PATCH] (feat): s2i build using local code Now user can provide just the image name and code directory and kedge build will create the configs and trigger binary build using openshift s2i source build strategy. --- cmd/build.go | 30 ++++++-- docs/user-guide.md | 19 ++++++ pkg/build/build.go | 154 ++++++++++++++++++++++++++++++++++++++++++ pkg/build/push.go | 13 +++- pkg/cmd/commands.go | 6 +- pkg/spec/resources.go | 7 +- 6 files changed, 216 insertions(+), 13 deletions(-) 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