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 c3d39e634d7462e16b89b58ee567322918a7bc95 Author: nferraro <ni.ferr...@gmail.com> AuthorDate: Thu Sep 6 10:41:48 2018 +0200 First e2e working version --- build/Makefile | 27 ++++- build/minishift_add_role.sh | 7 ++ cmd/camel-k/main.go | 6 +- deploy/cr.yaml | 18 ++++ pkg/apis/camel/v1alpha1/types.go | 5 +- pkg/build/build_manager.go | 12 +++ ...r_test.go => build_manager_integration_test.go} | 71 +++++++------ pkg/build/local/local_builder.go | 85 ++++++++++++---- ...r_test.go => local_builder_integration_test.go} | 8 +- pkg/stub/action/action.go | 3 + pkg/stub/action/build.go | 9 +- pkg/stub/action/deploy.go | 111 +++++++++++++++++++++ pkg/stub/action/initialize.go | 12 ++- pkg/stub/handler.go | 3 + .../action/action.go => util/test/testing_env.go} | 18 ++-- 15 files changed, 319 insertions(+), 76 deletions(-) diff --git a/build/Makefile b/build/Makefile index a818a4b..4d9c017 100644 --- a/build/Makefile +++ b/build/Makefile @@ -1,7 +1,30 @@ VERSION := $(shell ./build/get_version.sh) -compile: +install: go build -o camel-k ./cmd/camel-k/*.go images: - operator-sdk build docker.io/apache/camel-k:$(VERSION) \ No newline at end of file + operator-sdk build docker.io/apache/camel-k:$(VERSION) + +test: check +check: + go test ./... + +test-integration: check-integration +check-integration: require-kubeconfig require-namespace + go test ./... -tags=integration + +ifndef WATCH_NAMESPACE +require-namespace: + $(error WATCH_NAMESPACE not set) +else +require-namespace: + @echo "WATCH_NAMESPACE set" +endif +ifndef KUBERNETES_CONFIG +require-kubeconfig: + $(error KUBERNETES_CONFIG not set) +else +require-kubeconfig: + @echo "KUBERNETES_CONFIG set" +endif \ No newline at end of file diff --git a/build/minishift_add_role.sh b/build/minishift_add_role.sh new file mode 100755 index 0000000..4d69ca4 --- /dev/null +++ b/build/minishift_add_role.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +user=$(oc whoami) + +oc login -u system:admin +oc policy add-role-to-user --role-namespace=$(oc project -q) camel-k $user +oc login -u $user diff --git a/cmd/camel-k/main.go b/cmd/camel-k/main.go index 8fdaa53..7cd1f72 100644 --- a/cmd/camel-k/main.go +++ b/cmd/camel-k/main.go @@ -22,9 +22,9 @@ import ( "runtime" "time" - stub "github.com/apache/camel-k/pkg/stub" - sdk "github.com/operator-framework/operator-sdk/pkg/sdk" - k8sutil "github.com/operator-framework/operator-sdk/pkg/util/k8sutil" + "github.com/apache/camel-k/pkg/stub" + "github.com/operator-framework/operator-sdk/pkg/sdk" + "github.com/operator-framework/operator-sdk/pkg/util/k8sutil" sdkVersion "github.com/operator-framework/operator-sdk/version" "github.com/sirupsen/logrus" diff --git a/deploy/cr.yaml b/deploy/cr.yaml index 7a208db..ae0ee37 100644 --- a/deploy/cr.yaml +++ b/deploy/cr.yaml @@ -2,3 +2,21 @@ apiVersion: "camel.apache.org/v1alpha1" kind: "Integration" metadata: name: "example" +spec: + replicas: 1 + source: + code: |- + package kamel; + + import org.apache.camel.builder.RouteBuilder; + + public class Routes extends RouteBuilder { + + @Override + public void configure() throws Exception { + from("timer:tick") + .setBody(constant("Hello World!!!")) + .to("log:info"); + } + + } diff --git a/pkg/apis/camel/v1alpha1/types.go b/pkg/apis/camel/v1alpha1/types.go index b8ed15a..0f58ff9 100644 --- a/pkg/apis/camel/v1alpha1/types.go +++ b/pkg/apis/camel/v1alpha1/types.go @@ -48,8 +48,9 @@ type SourceSpec struct { } type IntegrationStatus struct { - Phase IntegrationPhase `json:"phase,omitempty"` - Identifier string `json:"identifier,omitempty"` + Phase IntegrationPhase `json:"phase,omitempty"` + Hash string `json:"hash,omitempty"` + Image string `json:"image,omitempty"` } type IntegrationPhase string diff --git a/pkg/build/build_manager.go b/pkg/build/build_manager.go index 7ddd03a..fee2315 100644 --- a/pkg/build/build_manager.go +++ b/pkg/build/build_manager.go @@ -50,6 +50,12 @@ func (m *BuildManager) Get(identifier string) api.BuildResult { } func (m *BuildManager) Start(source api.BuildSource) { + m.mutex.Lock() + defer m.mutex.Unlock() + + initialBuildInfo := initialBuildInfo() + m.builds[source.Identifier] = &initialBuildInfo + resChannel := m.builder.Build(source) go func() { res := <-resChannel @@ -64,4 +70,10 @@ func noBuildInfo() api.BuildResult { return api.BuildResult{ Status: api.BuildStatusNotRequested, } +} + +func initialBuildInfo() api.BuildResult { + return api.BuildResult{ + Status: api.BuildStatusStarted, + } } \ No newline at end of file diff --git a/pkg/build/local/local_builder_test.go b/pkg/build/build_manager_integration_test.go similarity index 51% copy from pkg/build/local/local_builder_test.go copy to pkg/build/build_manager_integration_test.go index f179682..f74fd4e 100644 --- a/pkg/build/local/local_builder_test.go +++ b/pkg/build/build_manager_integration_test.go @@ -1,3 +1,5 @@ +// +build integration + /* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -15,61 +17,64 @@ See the License for the specific language governing permissions and limitations under the License. */ -package local +package build import ( "testing" "context" "github.com/stretchr/testify/assert" build "github.com/apache/camel-k/pkg/build/api" + "time" + "github.com/apache/camel-k/pkg/util/test" ) 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(), - }) + buildManager := NewBuildManager(ctx, test.GetTargetNamespace()) - execution2 := builder.Build(build.BuildSource{ + buildManager.Start(build.BuildSource{ + Identifier: "1", Code: code(), }) - res1 := <- execution1 - res2 := <- execution2 - - assert.Nil(t, res1.Error, "Build failed") - assert.Nil(t, res2.Error, "Build failed") + deadline := time.Now().Add(5 * time.Minute) + var result build.BuildResult + for time.Now().Before(deadline) { + result = buildManager.Get("1") + if result.Status == build.BuildStatusCompleted || result.Status == build.BuildStatusError { + break + } + time.Sleep(2 * time.Second) + } + + assert.NotEqual(t, build.BuildStatusError, result.Status) + assert.Equal(t, build.BuildStatusCompleted, result.Status) + assert.Regexp(t, ".*/.*/.*:.*", result.Image) } func TestFailedBuild(t *testing.T) { ctx := context.TODO() - builder := NewLocalBuilder(ctx, "test") + buildManager := NewBuildManager(ctx, test.GetTargetNamespace()) - execution := builder.Build(build.BuildSource{ - Code: code() + "-", + buildManager.Start(build.BuildSource{ + Identifier: "1", + Code: code() + "XX", }) - res := <- execution - - assert.NotNil(t, res.Error, "Build should fail") + deadline := time.Now().Add(5 * time.Minute) + var result build.BuildResult + for time.Now().Before(deadline) { + result = buildManager.Get("1") + if result.Status == build.BuildStatusCompleted || result.Status == build.BuildStatusError { + break + } + time.Sleep(2 * time.Second) + } + + assert.Equal(t, build.BuildStatusError, result.Status) + assert.NotEqual(t, build.BuildStatusCompleted, result.Status) + assert.Empty(t, result.Image) } func code() string { diff --git a/pkg/build/local/local_builder.go b/pkg/build/local/local_builder.go index bdbb3d6..9874434 100644 --- a/pkg/build/local/local_builder.go +++ b/pkg/build/local/local_builder.go @@ -82,7 +82,7 @@ func (b *localBuilder) buildCycle(ctx context.Context) { case op := <- b.buffer: now := time.Now() logrus.Info("Starting new build") - err := b.execute(op.source) + image, err := b.execute(op.source) elapsed := time.Now().Sub(now) if err != nil { logrus.Error("Error during build (total time ", elapsed.Seconds(), " seconds): ", err) @@ -100,7 +100,7 @@ func (b *localBuilder) buildCycle(ctx context.Context) { op.output <- build.BuildResult{ Source: &op.source, Status: build.BuildStatusCompleted, - Image: "kamel:latest", + Image: image, } } @@ -108,10 +108,10 @@ func (b *localBuilder) buildCycle(ctx context.Context) { } } -func (b *localBuilder) execute(source build.BuildSource) error { +func (b *localBuilder) execute(source build.BuildSource) (string, error) { buildDir, err := ioutil.TempDir("", "kamel-") if err != nil { - return errors.Wrap(err, "could not create temporary dir for maven artifacts") + return "", errors.Wrap(err, "could not create temporary dir for maven artifacts") } //defer os.RemoveAll(buildDir) @@ -119,19 +119,19 @@ func (b *localBuilder) execute(source build.BuildSource) error { tarFileName, err := b.createTar(buildDir, source) if err != nil { - return err + return "", err } logrus.Info("Created tar file ", tarFileName) - err = b.publish(tarFileName, source) + image, err := b.publish(tarFileName, source) if err != nil { - return errors.Wrap(err, "could not publish docker image") + return "", errors.Wrap(err, "could not publish docker image") } - return nil + return image, nil } -func (b *localBuilder) publish(tarFile string, source build.BuildSource) error { +func (b *localBuilder) publish(tarFile string, source build.BuildSource) (string, error) { bc := buildv1.BuildConfig{ TypeMeta: metav1.TypeMeta{ @@ -168,7 +168,7 @@ func (b *localBuilder) publish(tarFile string, source build.BuildSource) error { sdk.Delete(&bc) err := sdk.Create(&bc) if err != nil { - return errors.Wrap(err, "cannot create build config") + return "", errors.Wrap(err, "cannot create build config") } is := imagev1.ImageStream{ @@ -190,7 +190,7 @@ func (b *localBuilder) publish(tarFile string, source build.BuildSource) error { sdk.Delete(&is) err = sdk.Create(&is) if err != nil { - return errors.Wrap(err, "cannot create image stream") + return "", errors.Wrap(err, "cannot create image stream") } inConfig := k8sclient.GetKubeConfig() @@ -211,12 +211,12 @@ func (b *localBuilder) publish(tarFile string, source build.BuildSource) error { restClient, err := rest.RESTClientFor(config) if err != nil { - return err + return "", err } resource, err := ioutil.ReadFile(tarFile) if err != nil { - return errors.Wrap(err, "cannot fully read tar file " + tarFile) + return "", errors.Wrap(err, "cannot fully read tar file " + tarFile) } result := restClient. @@ -229,25 +229,24 @@ func (b *localBuilder) publish(tarFile string, source build.BuildSource) error { Do() if result.Error() != nil { - return errors.Wrap(result.Error(), "cannot instantiate binary") + return "", errors.Wrap(result.Error(), "cannot instantiate binary") } data, err := result.Raw() if err != nil { - return errors.Wrap(err, "no raw data retrieved") + 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") + return "", errors.Wrap(err, "cannot unmarshal instantiate binary response") } ocbuild, err := k8sutil.RuntimeObjectFromUnstructured(&u) if err != nil { - return err + return "", err } - logrus.Info(ocbuild) err = kubernetes.WaitCondition(ocbuild, func(obj interface{})(bool, error) { if val, ok := obj.(*buildv1.Build); ok { @@ -262,7 +261,15 @@ func (b *localBuilder) publish(tarFile string, source build.BuildSource) error { return false, nil }, 5 * time.Minute) - return err + err = sdk.Get(&is) + if err != nil { + return "", err + } + + if is.Status.DockerImageRepository == "" { + return "", errors.New("dockerImageRepository not available in ImageStream") + } + return is.Status.DockerImageRepository + ":latest", nil } func (b *localBuilder) createTar(buildDir string, source build.BuildSource) (string, error) { @@ -385,6 +392,22 @@ func (b *localBuilder) createMavenStructure(buildDir string, source build.BuildS return "", err } + resourcesDir := path.Join(buildDir, "src", "main", "resources") + err = os.MkdirAll(resourcesDir, 0777) + if err != nil { + return "", err + } + log4jFileName := path.Join(resourcesDir, "log4j2.properties") + log4jFile, err := os.Create(log4jFileName) + if err != nil { + return "", err + } + defer log4jFile.Close() + + _, err = log4jFile.WriteString(b.log4jFile()) + if err != nil { + return "", err + } envFileName := path.Join(buildDir, "environment") envFile, err := os.Create(envFileName) @@ -426,8 +449,32 @@ func (b *localBuilder) createPom() string { <artifactId>camel-java-runtime</artifactId> <version>1.0-SNAPSHOT</version> </dependency> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-api</artifactId> + <version>2.11.1</version> + </dependency> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-core</artifactId> + <version>2.11.1</version> + </dependency> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-slf4j-impl</artifactId> + <version>2.11.1</version> + </dependency> </dependencies> </project> ` } + +func (b *localBuilder) log4jFile() string { + return `appender.out.type = Console +appender.out.name = out +appender.out.layout.type = PatternLayout +appender.out.layout.pattern = [%30.30t] %-30.30c{1} %-5p %m%n +rootLogger.level = INFO +rootLogger.appenderRef.out.ref = out` +} \ No newline at end of file diff --git a/pkg/build/local/local_builder_test.go b/pkg/build/local/local_builder_integration_test.go similarity index 90% rename from pkg/build/local/local_builder_test.go rename to pkg/build/local/local_builder_integration_test.go index f179682..ff14663 100644 --- a/pkg/build/local/local_builder_test.go +++ b/pkg/build/local/local_builder_integration_test.go @@ -1,3 +1,5 @@ +// +build integration + /* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -27,7 +29,7 @@ import ( func TestBuild(t *testing.T) { ctx := context.TODO() - builder := NewLocalBuilder(ctx, "test") + builder := NewLocalBuilder(ctx, test.GetTargetNamespace()) execution := builder.Build(build.BuildSource{ Code: code(), @@ -41,7 +43,7 @@ func TestBuild(t *testing.T) { func TestDoubleBuild(t *testing.T) { ctx := context.TODO() - builder := NewLocalBuilder(ctx, "test") + builder := NewLocalBuilder(ctx, test.GetTargetNamespace()) execution1 := builder.Build(build.BuildSource{ Code: code(), @@ -61,7 +63,7 @@ func TestDoubleBuild(t *testing.T) { func TestFailedBuild(t *testing.T) { ctx := context.TODO() - builder := NewLocalBuilder(ctx, "test") + builder := NewLocalBuilder(ctx, test.GetTargetNamespace()) execution := builder.Build(build.BuildSource{ Code: code() + "-", diff --git a/pkg/stub/action/action.go b/pkg/stub/action/action.go index d7558ce..c6d5cc2 100644 --- a/pkg/stub/action/action.go +++ b/pkg/stub/action/action.go @@ -23,6 +23,9 @@ import ( type Action interface { + // a user friendly name for the action + Name() string + // returns true if the action can handle the integration CanHandle(integration *v1alpha1.Integration) bool diff --git a/pkg/stub/action/build.go b/pkg/stub/action/build.go index ccae198..3b7b942 100644 --- a/pkg/stub/action/build.go +++ b/pkg/stub/action/build.go @@ -36,16 +36,20 @@ func NewBuildAction(ctx context.Context, namespace string) *BuildAction { } } +func (b *BuildAction) Name() string { + return "build" +} + 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) + buildResult := b.buildManager.Get(integration.Status.Hash) if buildResult.Status == api.BuildStatusNotRequested { b.buildManager.Start(api.BuildSource{ - Identifier: integration.Status.Identifier, + Identifier: integration.Status.Hash, Code: *integration.Spec.Source.Code, // FIXME possible panic }) logrus.Info("Build started") @@ -55,6 +59,7 @@ func (b *BuildAction) Handle(integration *v1alpha1.Integration) error { return sdk.Update(target) } else if buildResult.Status == api.BuildStatusCompleted { target := integration.DeepCopy() + target.Status.Image = buildResult.Image target.Status.Phase = v1alpha1.IntegrationPhaseDeploying return sdk.Update(target) } diff --git a/pkg/stub/action/deploy.go b/pkg/stub/action/deploy.go new file mode 100644 index 0000000..aa9f0df --- /dev/null +++ b/pkg/stub/action/deploy.go @@ -0,0 +1,111 @@ +/* +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" + "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1 "k8s.io/api/core/v1" + "github.com/operator-framework/operator-sdk/pkg/sdk" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "github.com/pkg/errors" +) + +type DeployAction struct { +} + +func NewDeployAction() *DeployAction { + return &DeployAction{} +} + +func (b *DeployAction) Name() string { + return "deploy" +} + +func (a *DeployAction) CanHandle(integration *v1alpha1.Integration) bool { + return integration.Status.Phase == v1alpha1.IntegrationPhaseDeploying +} + +func (a *DeployAction) Handle(integration *v1alpha1.Integration) error { + + deployment := a.getDeploymentFor(integration) + err := sdk.Create(deployment) + if err != nil && k8serrors.IsAlreadyExists(err) { + err = sdk.Update(deployment) + } + + if err != nil { + return errors.Wrap(err, "could not create or replace deployment for integration " + integration.Name) + } + + target := integration.DeepCopy() + target.Status.Phase = v1alpha1.IntegrationPhaseRunning + return sdk.Update(target) +} + +func (*DeployAction) getDeploymentFor(integration *v1alpha1.Integration) *v1.Deployment { + controller := true + blockOwnerDeletion := true + labels := map[string]string{ + "camel.apache.org/integration": integration.Name, + } + deployment := v1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: v1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: integration.Name, + Namespace: integration.Namespace, + Labels: integration.Labels, + Annotations: integration.Annotations, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: integration.APIVersion, + Kind: integration.Kind, + Name: integration.Name, + Controller: &controller, + BlockOwnerDeletion: &blockOwnerDeletion, + UID: integration.UID, + }, + }, + }, + Spec: v1.DeploymentSpec{ + Replicas: integration.Spec.Replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: integration.Name, + Image: integration.Status.Image, + }, + }, + }, + }, + }, + } + + return &deployment +} diff --git a/pkg/stub/action/initialize.go b/pkg/stub/action/initialize.go index 60f8dc6..52d8db9 100644 --- a/pkg/stub/action/initialize.go +++ b/pkg/stub/action/initialize.go @@ -33,13 +33,23 @@ func NewInitializeAction() *InitializeAction { return &InitializeAction{} } +func (b *InitializeAction) Name() string { + return "initialize" +} + func (b *InitializeAction) CanHandle(integration *v1alpha1.Integration) bool { return integration.Status.Phase == "" } func (b *InitializeAction) Handle(integration *v1alpha1.Integration) error { target := integration.DeepCopy() + // set default values + var defaultReplicas int32 = 1 + if target.Spec.Replicas == nil { + target.Spec.Replicas = &defaultReplicas + } + // update the status target.Status.Phase = v1alpha1.IntegrationPhaseBuilding - target.Status.Identifier = strconv.Itoa(rand.Int()) // TODO replace with hash + target.Status.Hash = 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 7c52247..ca46e54 100644 --- a/pkg/stub/handler.go +++ b/pkg/stub/handler.go @@ -24,6 +24,7 @@ import ( "github.com/operator-framework/operator-sdk/pkg/sdk" "github.com/apache/camel-k/pkg/stub/action" + "github.com/sirupsen/logrus" ) func NewHandler(ctx context.Context, namespace string) sdk.Handler { @@ -31,6 +32,7 @@ func NewHandler(ctx context.Context, namespace string) sdk.Handler { actionPool: []action.Action{ action.NewInitializeAction(), action.NewBuildAction(ctx, namespace), + action.NewDeployAction(), }, } } @@ -44,6 +46,7 @@ func (h *Handler) Handle(ctx context.Context, event sdk.Event) error { case *v1alpha1.Integration: for _, a := range h.actionPool { if a.CanHandle(o) { + logrus.Info("Invoking action ", a.Name(), " on integration ", o.Name) if err := a.Handle(o); err != nil { return err } diff --git a/pkg/stub/action/action.go b/pkg/util/test/testing_env.go similarity index 72% copy from pkg/stub/action/action.go copy to pkg/util/test/testing_env.go index d7558ce..16e7e13 100644 --- a/pkg/stub/action/action.go +++ b/pkg/util/test/testing_env.go @@ -15,18 +15,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package action +package test -import ( - "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" -) - -type Action interface { +import "os" - // returns true if the action can handle the integration - CanHandle(integration *v1alpha1.Integration) bool - - // executes the handling function - Handle(integration *v1alpha1.Integration) error +const ( + EnvWatchNamespace = "WATCH_NAMESPACE" +) +func GetTargetNamespace() string { + return os.Getenv(EnvWatchNamespace) }