You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by nf...@apache.org on 2018/09/06 11:06:31 UTC

[camel-k] 03/05: Initial draft of local build system

This is an automated email from the ASF dual-hosted git repository.

nferraro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/camel-k.git

commit 8661409fac1c95481ac5b25cb1db648f687a2d10
Author: nferraro <ni...@gmail.com>
AuthorDate: Mon Sep 3 09:49:57 2018 +0200

    Initial draft of local build system
---
 Gopkg.lock                                    |  36 ++-
 cmd/camel-k/main.go                           |   5 +-
 pkg/apis/camel/v1alpha1/types.go              |   4 +-
 pkg/build/{build.go => api/types.go}          |  28 +-
 pkg/build/build_manager.go                    |  67 ++++
 pkg/build/local/local_builder.go              | 433 ++++++++++++++++++++++++++
 pkg/build/local/local_builder_test.go         |  90 ++++++
 pkg/build/local/scheme.go                     |  98 ++++++
 pkg/stub/action/action.go                     |   6 +-
 pkg/stub/action/build.go                      |  63 ++++
 pkg/stub/{handler.go => action/initialize.go} |  40 +--
 pkg/stub/handler.go                           |  11 +-
 pkg/util/kubernetes/wait.go                   |  39 +++
 pkg/util/openshift/register.go                |  77 +++++
 14 files changed, 963 insertions(+), 34 deletions(-)

diff --git a/Gopkg.lock b/Gopkg.lock
index c662f8a..08cdcda 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -169,6 +169,22 @@
   version = "1.0.1"
 
 [[projects]]
+  name = "github.com/openshift/api"
+  packages = [
+    "apps/v1",
+    "authorization/v1",
+    "build/v1",
+    "image/docker10",
+    "image/dockerpre012",
+    "image/v1",
+    "pkg/serialization",
+    "route/v1",
+    "template/v1"
+  ]
+  revision = "0d921e363e951d89f583292c60d013c318df64dc"
+  version = "v3.9.0"
+
+[[projects]]
   branch = "master"
   name = "github.com/operator-framework/operator-sdk"
   packages = [
@@ -181,6 +197,18 @@
   revision = "0f60da86a138f7e79f275d539c18615bc2727014"
 
 [[projects]]
+  name = "github.com/pkg/errors"
+  packages = ["."]
+  revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
+  version = "v0.8.0"
+
+[[projects]]
+  name = "github.com/pmezard/go-difflib"
+  packages = ["difflib"]
+  revision = "792786c7400a136282c1664665ae0a8db921c6c2"
+  version = "v1.0.0"
+
+[[projects]]
   name = "github.com/prometheus/client_golang"
   packages = [
     "prometheus",
@@ -229,6 +257,12 @@
   version = "v1.0.2"
 
 [[projects]]
+  name = "github.com/stretchr/testify"
+  packages = ["assert"]
+  revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
+  version = "v1.2.2"
+
+[[projects]]
   branch = "master"
   name = "golang.org/x/crypto"
   packages = ["ssh/terminal"]
@@ -538,6 +572,6 @@
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "22a3131aff0c3b6f0a97bc22324996cfa5dabe1f626a90facb0daf29bbe0d52a"
+  inputs-digest = "fe3fc8a50615445124a1a85023b09a54b2aa9f0dfc077f6c4823b46ee4ba7ef1"
   solver-name = "gps-cdcl"
   solver-version = 1
diff --git a/cmd/camel-k/main.go b/cmd/camel-k/main.go
index 63c4fc0..8fdaa53 100644
--- a/cmd/camel-k/main.go
+++ b/cmd/camel-k/main.go
@@ -48,9 +48,10 @@ func main() {
 	if err != nil {
 		logrus.Fatalf("failed to get watch namespace: %v", err)
 	}
+	ctx := context.TODO()
 	resyncPeriod := time.Duration(5) * time.Second
 	logrus.Infof("Watching %s, %s, %s, %d", resource, kind, namespace, resyncPeriod)
 	sdk.Watch(resource, kind, namespace, resyncPeriod)
-	sdk.Handle(stub.NewHandler())
-	sdk.Run(context.TODO())
+	sdk.Handle(stub.NewHandler(ctx, namespace))
+	sdk.Run(ctx)
 }
diff --git a/pkg/apis/camel/v1alpha1/types.go b/pkg/apis/camel/v1alpha1/types.go
index d1a4431..b8ed15a 100644
--- a/pkg/apis/camel/v1alpha1/types.go
+++ b/pkg/apis/camel/v1alpha1/types.go
@@ -48,13 +48,15 @@ type SourceSpec struct {
 }
 
 type IntegrationStatus struct {
-	Phase   IntegrationPhase    `json:"phase,omitempty"`
+	Phase   	IntegrationPhase    `json:"phase,omitempty"`
+	Identifier	string				`json:"identifier,omitempty"`
 }
 
 type IntegrationPhase string
 
 const (
     IntegrationPhaseBuilding    IntegrationPhase = "Building"
+	IntegrationPhaseDeploying   IntegrationPhase = "Deploying"
     IntegrationPhaseRunning     IntegrationPhase = "Running"
     IntegrationPhaseError       IntegrationPhase = "Error"
 )
\ No newline at end of file
diff --git a/pkg/build/build.go b/pkg/build/api/types.go
similarity index 61%
rename from pkg/build/build.go
rename to pkg/build/api/types.go
index 88d762c..4500937 100644
--- a/pkg/build/build.go
+++ b/pkg/build/api/types.go
@@ -15,5 +15,31 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package build
+package api
 
+// a request to build a specific code
+type BuildSource struct {
+	Identifier	string
+	Code		string
+}
+
+// represents the result of a build
+type BuildResult struct {
+	Source		*BuildSource
+	Status		BuildStatus
+	Image		string
+	Error		error
+}
+
+// supertype of all builders
+type Builder interface {
+	Build(BuildSource) <- chan BuildResult
+}
+
+type BuildStatus int
+const (
+	BuildStatusNotRequested		BuildStatus = iota
+	BuildStatusStarted
+	BuildStatusCompleted
+	BuildStatusError
+)
\ No newline at end of file
diff --git a/pkg/build/build_manager.go b/pkg/build/build_manager.go
new file mode 100644
index 0000000..7ddd03a
--- /dev/null
+++ b/pkg/build/build_manager.go
@@ -0,0 +1,67 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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 build
+
+import (
+	"sync"
+	"github.com/apache/camel-k/pkg/build/local"
+	"context"
+	"github.com/apache/camel-k/pkg/build/api"
+)
+
+// main facade to the image build system
+type BuildManager struct {
+	builds	map[string]*api.BuildResult
+	mutex	sync.Mutex
+	builder	api.Builder
+}
+
+func NewBuildManager(ctx context.Context, namespace string) *BuildManager {
+	return &BuildManager{
+		builds: make(map[string]*api.BuildResult),
+		builder: local.NewLocalBuilder(ctx, namespace),
+	}
+}
+
+func (m *BuildManager) Get(identifier string) api.BuildResult {
+	m.mutex.Lock()
+	defer m.mutex.Unlock()
+
+	if info, present := m.builds[identifier]; !present || info == nil {
+		return noBuildInfo()
+	} else {
+		return *info
+	}
+}
+
+func (m *BuildManager) Start(source api.BuildSource) {
+	resChannel := m.builder.Build(source)
+	go func() {
+		res := <-resChannel
+		m.mutex.Lock()
+		defer m.mutex.Unlock()
+
+		m.builds[res.Source.Identifier] = &res
+	}()
+}
+
+func noBuildInfo() api.BuildResult {
+	return api.BuildResult{
+		Status: api.BuildStatusNotRequested,
+	}
+}
\ No newline at end of file
diff --git a/pkg/build/local/local_builder.go b/pkg/build/local/local_builder.go
new file mode 100644
index 0000000..bdbb3d6
--- /dev/null
+++ b/pkg/build/local/local_builder.go
@@ -0,0 +1,433 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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 local
+
+import (
+	"context"
+	"github.com/sirupsen/logrus"
+	"time"
+	"io/ioutil"
+	"path"
+	"os"
+	"os/exec"
+	"github.com/pkg/errors"
+	"archive/tar"
+	"io"
+	buildv1 "github.com/openshift/api/build/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/api/core/v1"
+	"github.com/operator-framework/operator-sdk/pkg/sdk"
+	"k8s.io/client-go/rest"
+	"github.com/operator-framework/operator-sdk/pkg/k8sclient"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	imagev1 "github.com/openshift/api/image/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"github.com/operator-framework/operator-sdk/pkg/util/k8sutil"
+
+	_ "github.com/apache/camel-k/pkg/util/openshift"
+	build "github.com/apache/camel-k/pkg/build/api"
+	"github.com/apache/camel-k/pkg/util/kubernetes"
+)
+
+type localBuilder struct {
+	buffer     chan buildOperation
+	namespace  string
+}
+
+type buildOperation struct {
+	source	build.BuildSource
+	output	chan build.BuildResult
+}
+
+func NewLocalBuilder(ctx context.Context, namespace string) build.Builder {
+	builder := localBuilder{
+		buffer: make(chan buildOperation, 100),
+		namespace: namespace,
+	}
+	go builder.buildCycle(ctx)
+	return &builder
+}
+
+func (b *localBuilder) Build(source build.BuildSource) <- chan build.BuildResult {
+	res := make(chan build.BuildResult, 1)
+	op := buildOperation{
+		source: source,
+		output: res,
+	}
+	b.buffer <- op
+	return res
+}
+
+func (b *localBuilder) buildCycle(ctx context.Context) {
+	for {
+		select {
+		case <- ctx.Done():
+			b.buffer = nil
+			return
+		case op := <- b.buffer:
+			now := time.Now()
+			logrus.Info("Starting new build")
+			err := b.execute(op.source)
+			elapsed := time.Now().Sub(now)
+			if err != nil {
+				logrus.Error("Error during build (total time ", elapsed.Seconds(), " seconds): ", err)
+			} else {
+				logrus.Info("Build completed in ", elapsed.Seconds(), " seconds")
+			}
+
+			if err != nil {
+				op.output <- build.BuildResult{
+					Source: &op.source,
+					Status: build.BuildStatusError,
+					Error: err,
+				}
+			} else {
+				op.output <- build.BuildResult{
+					Source: &op.source,
+					Status: build.BuildStatusCompleted,
+					Image: "kamel:latest",
+				}
+			}
+
+		}
+	}
+}
+
+func (b *localBuilder) execute(source build.BuildSource) error {
+	buildDir, err := ioutil.TempDir("", "kamel-")
+	if err != nil {
+		return errors.Wrap(err, "could not create temporary dir for maven artifacts")
+	}
+	//defer os.RemoveAll(buildDir)
+
+	logrus.Info("Using build dir : ", buildDir)
+
+	tarFileName, err := b.createTar(buildDir, source)
+	if err != nil {
+		return err
+	}
+	logrus.Info("Created tar file ", tarFileName)
+
+	err = b.publish(tarFileName, source)
+	if err != nil {
+		return errors.Wrap(err, "could not publish docker image")
+	}
+
+	return nil
+}
+
+func (b *localBuilder) publish(tarFile string, source build.BuildSource) error {
+
+	bc := buildv1.BuildConfig{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: buildv1.SchemeGroupVersion.String(),
+			Kind: "BuildConfig",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "kamel",
+			Namespace: b.namespace,
+		},
+		Spec: buildv1.BuildConfigSpec{
+			CommonSpec: buildv1.CommonSpec{
+				Source: buildv1.BuildSource{
+					Type: buildv1.BuildSourceBinary,
+				},
+				Strategy: buildv1.BuildStrategy{
+					SourceStrategy: &buildv1.SourceBuildStrategy{
+						From: v1.ObjectReference{
+							Kind: "DockerImage",
+							Name: "fabric8/s2i-java:2.1",
+						},
+					},
+				},
+				Output: buildv1.BuildOutput{
+					To: &v1.ObjectReference{
+						Kind: "ImageStreamTag",
+						Name: "kamel:latest",
+					},
+				},
+			},
+		},
+	}
+
+	sdk.Delete(&bc)
+	err := sdk.Create(&bc)
+	if err != nil {
+		return errors.Wrap(err, "cannot create build config")
+	}
+
+	is := imagev1.ImageStream{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: imagev1.SchemeGroupVersion.String(),
+			Kind: "ImageStream",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "kamel",
+			Namespace: b.namespace,
+		},
+		Spec: imagev1.ImageStreamSpec{
+			LookupPolicy: imagev1.ImageLookupPolicy{
+				Local: true,
+			},
+		},
+	}
+
+	sdk.Delete(&is)
+	err = sdk.Create(&is)
+	if err != nil {
+		return errors.Wrap(err, "cannot create image stream")
+	}
+
+	inConfig := k8sclient.GetKubeConfig()
+	config := rest.CopyConfig(inConfig)
+	config.GroupVersion = &schema.GroupVersion{
+		Group: "build.openshift.io",
+		Version: "v1",
+	}
+	config.APIPath = "/apis"
+	config.AcceptContentTypes = "application/json"
+	config.ContentType = "application/json"
+
+	// this gets used for discovery and error handling types
+	config.NegotiatedSerializer = basicNegotiatedSerializer{}
+	if config.UserAgent == "" {
+		config.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	restClient, err := rest.RESTClientFor(config)
+	if err != nil {
+		return err
+	}
+
+	resource, err := ioutil.ReadFile(tarFile)
+	if err != nil {
+		return errors.Wrap(err, "cannot fully read tar file " + tarFile)
+	}
+
+	result := restClient.
+		Post().
+		Namespace(b.namespace).
+		Body(resource).
+		Resource("buildconfigs").
+		Name("kamel").
+		SubResource("instantiatebinary").
+		Do()
+
+	if result.Error() != nil {
+		return errors.Wrap(result.Error(), "cannot instantiate binary")
+	}
+
+	data, err := result.Raw()
+	if err != nil {
+		return errors.Wrap(err, "no raw data retrieved")
+	}
+
+	u := unstructured.Unstructured{}
+	err = u.UnmarshalJSON(data)
+	if err != nil {
+		return errors.Wrap(err, "cannot unmarshal instantiate binary response")
+	}
+
+	ocbuild, err := k8sutil.RuntimeObjectFromUnstructured(&u)
+	if err != nil {
+		return err
+	}
+	logrus.Info(ocbuild)
+
+	err = kubernetes.WaitCondition(ocbuild, func(obj interface{})(bool, error) {
+		if val, ok := obj.(*buildv1.Build); ok {
+			if val.Status.Phase == buildv1.BuildPhaseComplete {
+				return true, nil
+			} else if val.Status.Phase == buildv1.BuildPhaseCancelled ||
+				val.Status.Phase == buildv1.BuildPhaseFailed ||
+				val.Status.Phase == buildv1.BuildPhaseError {
+				return false, errors.New("build failed")
+			}
+		}
+		return false, nil
+	}, 5 * time.Minute)
+
+	return err
+}
+
+func (b *localBuilder) createTar(buildDir string, source build.BuildSource) (string, error) {
+	dir, err := b.createMavenStructure(buildDir, source)
+	if err != nil {
+		return "", err
+	}
+
+	mavenBuild := exec.Command("mvn", "clean", "install", "-DskipTests")
+	mavenBuild.Dir = dir
+	logrus.Info("Starting maven build: mvn clean install -DskipTests")
+	err = mavenBuild.Run()
+	if err != nil {
+		return "", errors.Wrap(err, "failure while executing maven build")
+	}
+
+	mavenDep := exec.Command("mvn", "dependency:copy-dependencies")
+	mavenDep.Dir = dir
+	logrus.Info("Copying maven dependencies: mvn dependency:copy-dependencies")
+	err = mavenDep.Run()
+	if err != nil {
+		return "", errors.Wrap(err, "failure while extracting maven dependencies")
+	}
+	logrus.Info("Maven build completed successfully")
+
+	tarFileName := path.Join(buildDir, "kamel-app.tar")
+	tarFile, err := os.Create(tarFileName)
+	if err != nil {
+		return "", errors.Wrap(err, "cannot create tar file")
+	}
+	defer tarFile.Close()
+
+	writer := tar.NewWriter(tarFile)
+	err = b.appendToTar(path.Join(buildDir, "target", "kamel-app-1.0.0.jar"), "", writer)
+	if err != nil {
+		return "", err
+	}
+
+	err = b.appendToTar(path.Join(buildDir, "environment"), ".s2i", writer)
+	if err != nil {
+		return "", err
+	}
+
+	dependenciesDir := path.Join(buildDir, "target", "dependency")
+	dependencies, err := ioutil.ReadDir(dependenciesDir)
+	if err != nil {
+		return "", err
+	}
+
+	for _, dep := range dependencies {
+		err = b.appendToTar(path.Join(dependenciesDir, dep.Name()), "", writer)
+		if err != nil {
+			return "", err
+		}
+	}
+
+	writer.Close()
+
+	return tarFileName, nil
+}
+
+func (b *localBuilder) appendToTar(filePath string, tarPath string, writer *tar.Writer) error {
+	info, err := os.Stat(filePath)
+	if err != nil {
+		return err
+	}
+	_, fileName := path.Split(filePath)
+	if tarPath != "" {
+		fileName = path.Join(tarPath, fileName)
+	}
+
+	writer.WriteHeader(&tar.Header{
+		Name: fileName,
+		Size: info.Size(),
+		Mode: int64(info.Mode()),
+		ModTime: info.ModTime(),
+	})
+
+	file, err := os.Open(filePath)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+
+	_, err = io.Copy(writer, file)
+	if err != nil {
+		return errors.Wrap(err, "cannot add file to the tar archive")
+	}
+	return nil
+}
+
+func (b *localBuilder) createMavenStructure(buildDir string, source build.BuildSource) (string, error) {
+	pomFileName := path.Join(buildDir, "pom.xml")
+	pom, err := os.Create(pomFileName)
+	if err != nil {
+		return "", err
+	}
+	defer pom.Close()
+
+	_, err = pom.WriteString(b.createPom())
+	if err != nil {
+		return "", err
+	}
+
+	packageDir := path.Join(buildDir, "src", "main", "java", "kamel")
+	err = os.MkdirAll(packageDir, 0777)
+	if err != nil {
+		return "", err
+	}
+
+	sourceFileName := path.Join(packageDir, "Routes.java")
+	sourceFile, err := os.Create(sourceFileName)
+	if err != nil {
+		return "", err
+	}
+	defer sourceFile.Close()
+
+	_, err = sourceFile.WriteString(source.Code)
+	if err != nil {
+		return "", err
+	}
+
+
+	envFileName := path.Join(buildDir, "environment")
+	envFile, err := os.Create(envFileName)
+	if err != nil {
+		return "", err
+	}
+	defer envFile.Close()
+
+	_, err = envFile.WriteString(b.createEnvFile())
+	if err != nil {
+		return "", err
+	}
+
+	return buildDir, nil
+}
+
+func (b *localBuilder) createEnvFile() string {
+	return `
+JAVA_MAIN_CLASS=me.nicolaferraro.kamel.Application
+KAMEL_CLASS=kamel.Routes
+`
+}
+
+
+func (b *localBuilder) createPom() string {
+	return `<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>me.nicolaferraro.kamel.app</groupId>
+  <artifactId>kamel-app</artifactId>
+  <version>1.0.0</version>
+
+  <dependencies>
+    <dependency>
+      <groupId>me.nicolaferraro.kamel</groupId>
+      <artifactId>camel-java-runtime</artifactId>
+      <version>1.0-SNAPSHOT</version>
+    </dependency>
+  </dependencies>
+
+</project>
+`
+}
diff --git a/pkg/build/local/local_builder_test.go b/pkg/build/local/local_builder_test.go
new file mode 100644
index 0000000..f179682
--- /dev/null
+++ b/pkg/build/local/local_builder_test.go
@@ -0,0 +1,90 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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 local
+
+import (
+	"testing"
+	"context"
+	"github.com/stretchr/testify/assert"
+	build "github.com/apache/camel-k/pkg/build/api"
+)
+
+func TestBuild(t *testing.T) {
+
+	ctx := context.TODO()
+	builder := NewLocalBuilder(ctx, "test")
+
+	execution := builder.Build(build.BuildSource{
+		Code: code(),
+	})
+
+	res := <- execution
+
+	assert.Nil(t, res.Error, "Build failed")
+}
+
+func TestDoubleBuild(t *testing.T) {
+
+	ctx := context.TODO()
+	builder := NewLocalBuilder(ctx, "test")
+
+	execution1 := builder.Build(build.BuildSource{
+		Code: code(),
+	})
+
+	execution2 := builder.Build(build.BuildSource{
+		Code: code(),
+	})
+
+	res1 := <- execution1
+	res2 := <- execution2
+
+	assert.Nil(t, res1.Error, "Build failed")
+	assert.Nil(t, res2.Error, "Build failed")
+}
+
+func TestFailedBuild(t *testing.T) {
+
+	ctx := context.TODO()
+	builder := NewLocalBuilder(ctx, "test")
+
+	execution := builder.Build(build.BuildSource{
+		Code: code() + "-",
+	})
+
+	res := <- execution
+
+	assert.NotNil(t, res.Error, "Build should fail")
+}
+
+func code() string {
+	return `package kamel;
+
+import org.apache.camel.builder.RouteBuilder;
+
+public class Routes extends RouteBuilder {
+
+	@Override
+    public void configure() throws Exception {
+        from("timer:tick")
+		  .to("log:info");
+    }
+
+}
+`
+}
\ No newline at end of file
diff --git a/pkg/build/local/scheme.go b/pkg/build/local/scheme.go
new file mode 100644
index 0000000..7fdc19f
--- /dev/null
+++ b/pkg/build/local/scheme.go
@@ -0,0 +1,98 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+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 local
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/runtime/serializer"
+	"k8s.io/apimachinery/pkg/runtime/serializer/json"
+	"k8s.io/apimachinery/pkg/runtime/serializer/versioning"
+)
+
+var watchScheme = runtime.NewScheme()
+var basicScheme = runtime.NewScheme()
+var deleteScheme = runtime.NewScheme()
+var parameterScheme = runtime.NewScheme()
+var deleteOptionsCodec = serializer.NewCodecFactory(deleteScheme)
+var dynamicParameterCodec = runtime.NewParameterCodec(parameterScheme)
+
+var versionV1 = schema.GroupVersion{Version: "v1"}
+
+func init() {
+	metav1.AddToGroupVersion(watchScheme, versionV1)
+	metav1.AddToGroupVersion(basicScheme, versionV1)
+	metav1.AddToGroupVersion(parameterScheme, versionV1)
+	metav1.AddToGroupVersion(deleteScheme, versionV1)
+}
+
+var watchJsonSerializerInfo = runtime.SerializerInfo{
+	MediaType:        "application/json",
+	EncodesAsText:    true,
+	Serializer:       json.NewSerializer(json.DefaultMetaFactory, watchScheme, watchScheme, false),
+	PrettySerializer: json.NewSerializer(json.DefaultMetaFactory, watchScheme, watchScheme, true),
+	StreamSerializer: &runtime.StreamSerializerInfo{
+		EncodesAsText: true,
+		Serializer:    json.NewSerializer(json.DefaultMetaFactory, watchScheme, watchScheme, false),
+		Framer:        json.Framer,
+	},
+}
+
+// watchNegotiatedSerializer is used to read the wrapper of the watch stream
+type watchNegotiatedSerializer struct{}
+
+var watchNegotiatedSerializerInstance = watchNegotiatedSerializer{}
+
+func (s watchNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
+	return []runtime.SerializerInfo{watchJsonSerializerInfo}
+}
+
+func (s watchNegotiatedSerializer) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
+	return versioning.NewDefaultingCodecForScheme(watchScheme, encoder, nil, gv, nil)
+}
+
+func (s watchNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
+	return versioning.NewDefaultingCodecForScheme(watchScheme, nil, decoder, nil, gv)
+}
+
+// basicNegotiatedSerializer is used to handle discovery and error handling serialization
+type basicNegotiatedSerializer struct{}
+
+func (s basicNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
+	return []runtime.SerializerInfo{
+		{
+			MediaType:        "application/json",
+			EncodesAsText:    true,
+			Serializer:       json.NewSerializer(json.DefaultMetaFactory, basicScheme, basicScheme, false),
+			PrettySerializer: json.NewSerializer(json.DefaultMetaFactory, basicScheme, basicScheme, true),
+			StreamSerializer: &runtime.StreamSerializerInfo{
+				EncodesAsText: true,
+				Serializer:    json.NewSerializer(json.DefaultMetaFactory, basicScheme, basicScheme, false),
+				Framer:        json.Framer,
+			},
+		},
+	}
+}
+
+func (s basicNegotiatedSerializer) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
+	return versioning.NewDefaultingCodecForScheme(watchScheme, encoder, nil, gv, nil)
+}
+
+func (s basicNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
+	return versioning.NewDefaultingCodecForScheme(watchScheme, nil, decoder, nil, gv)
+}
\ No newline at end of file
diff --git a/pkg/stub/action/action.go b/pkg/stub/action/action.go
index 185105f..d7558ce 100644
--- a/pkg/stub/action/action.go
+++ b/pkg/stub/action/action.go
@@ -23,8 +23,10 @@ import (
 
 type Action interface {
 
-	CanExecute(integration *v1alpha1.Integration) bool
+	// returns true if the action can handle the integration
+	CanHandle(integration *v1alpha1.Integration) bool
 
-	Execute(syndesis *v1alpha1.Integration) error
+	// executes the handling function
+	Handle(integration *v1alpha1.Integration) error
 
 }
diff --git a/pkg/stub/action/build.go b/pkg/stub/action/build.go
new file mode 100644
index 0000000..ccae198
--- /dev/null
+++ b/pkg/stub/action/build.go
@@ -0,0 +1,63 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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 action
+
+import (
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"context"
+	"github.com/apache/camel-k/pkg/build"
+	"github.com/sirupsen/logrus"
+	"github.com/operator-framework/operator-sdk/pkg/sdk"
+	"github.com/apache/camel-k/pkg/build/api"
+)
+
+type BuildAction struct {
+	buildManager	*build.BuildManager
+}
+
+func NewBuildAction(ctx context.Context, namespace string) *BuildAction {
+	return &BuildAction{
+		buildManager: build.NewBuildManager(ctx, namespace),
+	}
+}
+
+func (b *BuildAction) CanHandle(integration *v1alpha1.Integration) bool {
+	return integration.Status.Phase == v1alpha1.IntegrationPhaseBuilding
+}
+
+func (b *BuildAction) Handle(integration *v1alpha1.Integration) error {
+
+	buildResult := b.buildManager.Get(integration.Status.Identifier)
+	if buildResult.Status == api.BuildStatusNotRequested {
+		b.buildManager.Start(api.BuildSource{
+			Identifier: integration.Status.Identifier,
+			Code: *integration.Spec.Source.Code, // FIXME possible panic
+		})
+		logrus.Info("Build started")
+	} else if buildResult.Status == api.BuildStatusError {
+		target := integration.DeepCopy()
+		target.Status.Phase = v1alpha1.IntegrationPhaseError
+		return sdk.Update(target)
+	} else if buildResult.Status == api.BuildStatusCompleted {
+		target := integration.DeepCopy()
+		target.Status.Phase = v1alpha1.IntegrationPhaseDeploying
+		return sdk.Update(target)
+	}
+
+	return nil
+}
diff --git a/pkg/stub/handler.go b/pkg/stub/action/initialize.go
similarity index 59%
copy from pkg/stub/handler.go
copy to pkg/stub/action/initialize.go
index 8f6fd16..60f8dc6 100644
--- a/pkg/stub/handler.go
+++ b/pkg/stub/action/initialize.go
@@ -15,37 +15,31 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package stub
+package action
 
 import (
-	"context"
-
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
-
 	"github.com/operator-framework/operator-sdk/pkg/sdk"
-	"github.com/apache/camel-k/pkg/stub/action"
+	"math/rand"
+	"strconv"
 )
 
-func NewHandler() sdk.Handler {
-	return &Handler{
-		actionPool: []action.Action{},
-	}
+// initializes the integration status to trigger the deployment
+type InitializeAction struct {
+
+}
+
+func NewInitializeAction() *InitializeAction {
+	return &InitializeAction{}
 }
 
-type Handler struct {
-	actionPool	[]action.Action
+func (b *InitializeAction) CanHandle(integration *v1alpha1.Integration) bool {
+	return integration.Status.Phase == ""
 }
 
-func (h *Handler) Handle(ctx context.Context, event sdk.Event) error {
-	switch o := event.Object.(type) {
-	case *v1alpha1.Integration:
-		for _, a := range h.actionPool {
-			if a.CanExecute(o) {
-				if err := a.Execute(o); err != nil {
-					return err
-				}
-			}
-		}
-	}
-	return nil
+func (b *InitializeAction) Handle(integration *v1alpha1.Integration) error {
+	target := integration.DeepCopy()
+	target.Status.Phase = v1alpha1.IntegrationPhaseBuilding
+	target.Status.Identifier = strconv.Itoa(rand.Int()) // TODO replace with hash
+	return sdk.Update(target)
 }
diff --git a/pkg/stub/handler.go b/pkg/stub/handler.go
index 8f6fd16..7c52247 100644
--- a/pkg/stub/handler.go
+++ b/pkg/stub/handler.go
@@ -26,9 +26,12 @@ import (
 	"github.com/apache/camel-k/pkg/stub/action"
 )
 
-func NewHandler() sdk.Handler {
+func NewHandler(ctx context.Context, namespace string) sdk.Handler {
 	return &Handler{
-		actionPool: []action.Action{},
+		actionPool: []action.Action{
+			action.NewInitializeAction(),
+			action.NewBuildAction(ctx, namespace),
+		},
 	}
 }
 
@@ -40,8 +43,8 @@ func (h *Handler) Handle(ctx context.Context, event sdk.Event) error {
 	switch o := event.Object.(type) {
 	case *v1alpha1.Integration:
 		for _, a := range h.actionPool {
-			if a.CanExecute(o) {
-				if err := a.Execute(o); err != nil {
+			if a.CanHandle(o) {
+				if err := a.Handle(o); err != nil {
 					return err
 				}
 			}
diff --git a/pkg/util/kubernetes/wait.go b/pkg/util/kubernetes/wait.go
new file mode 100644
index 0000000..f333b78
--- /dev/null
+++ b/pkg/util/kubernetes/wait.go
@@ -0,0 +1,39 @@
+package kubernetes
+
+import (
+	"time"
+	"github.com/pkg/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+	"github.com/operator-framework/operator-sdk/pkg/sdk"
+)
+
+type ResourceRetrieveFunction func()(interface{}, error)
+
+type ResourceCheckFunction func(interface{})(bool, error)
+
+const (
+	sleepTime = 400 * time.Millisecond
+)
+
+func WaitCondition(obj runtime.Object, condition ResourceCheckFunction, maxDuration time.Duration) error {
+	start := time.Now()
+
+	for start.Add(maxDuration).After(time.Now()) {
+		err := sdk.Get(obj)
+		if err != nil {
+			time.Sleep(sleepTime)
+			continue
+		}
+
+		satisfied, err := condition(obj)
+		if err != nil {
+			return errors.Wrap(err, "error while evaluating condition")
+		} else if !satisfied {
+			time.Sleep(sleepTime)
+			continue
+		}
+
+		return nil
+	}
+	return errors.New("timeout while waiting condition")
+}
\ No newline at end of file
diff --git a/pkg/util/openshift/register.go b/pkg/util/openshift/register.go
new file mode 100644
index 0000000..076db9f
--- /dev/null
+++ b/pkg/util/openshift/register.go
@@ -0,0 +1,77 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+*/
+
+// Register all Openshift types that we want to manage.
+package openshift
+
+import (
+	apps "github.com/openshift/api/apps/v1"
+	authorization "github.com/openshift/api/authorization/v1"
+	build "github.com/openshift/api/build/v1"
+	image "github.com/openshift/api/image/v1"
+	route "github.com/openshift/api/route/v1"
+	template "github.com/openshift/api/template/v1"
+	"github.com/operator-framework/operator-sdk/pkg/util/k8sutil"
+	"github.com/sirupsen/logrus"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func init() {
+	k8sutil.AddToSDKScheme(AddToScheme)
+}
+
+var (
+	AddToScheme = addKnownTypes
+)
+
+type registerFunction func(*runtime.Scheme) error
+
+func addKnownTypes(scheme *runtime.Scheme) error {
+
+	var err error
+
+	// Standardized groups
+	err = doAdd(apps.AddToScheme, scheme, err)
+	err = doAdd(template.AddToScheme, scheme, err)
+	err = doAdd(image.AddToScheme, scheme, err)
+	err = doAdd(route.AddToScheme, scheme, err)
+	err = doAdd(build.AddToScheme, scheme, err)
+	err = doAdd(authorization.AddToScheme, scheme, err)
+
+
+	// Legacy "oapi" resources
+	err = doAdd(apps.AddToSchemeInCoreGroup, scheme, err)
+	err = doAdd(template.AddToSchemeInCoreGroup, scheme, err)
+	err = doAdd(image.AddToSchemeInCoreGroup, scheme, err)
+	err = doAdd(route.AddToSchemeInCoreGroup, scheme, err)
+	err = doAdd(build.AddToSchemeInCoreGroup, scheme, err)
+	err = doAdd(authorization.AddToSchemeInCoreGroup, scheme, err)
+
+	return err
+}
+
+func doAdd(addToScheme registerFunction, scheme *runtime.Scheme, err error) error {
+	callErr := addToScheme(scheme)
+	if callErr != nil {
+		logrus.Error("Error while registering Openshift types", callErr)
+	}
+
+	if err == nil {
+		return callErr
+	}
+	return err
+}
\ No newline at end of file