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.ferr...@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