This is an automated email from the ASF dual-hosted git repository. lburgazzoli pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/camel-k.git
commit 07ea1787eb145bd5478ce4955bc35bf6b4413652 Author: nferraro <[email protected]> AuthorDate: Fri Sep 28 12:17:52 2018 +0200 First working version of Kaniko builder --- deploy/builder-pvc.yaml | 12 ++ deploy/operator-deployment.yaml | 7 + deploy/resources.go | 23 +++ pkg/build/build_manager.go | 30 ++- pkg/build/build_types.go | 15 +- pkg/build/packager/base.go | 172 ++++++++++++++++ pkg/{install/operator.go => build/packager/doc.go} | 26 +-- pkg/build/packager/factory.go | 49 +++++ .../incremental.go} | 45 ++--- .../operator.go => build/packager/types.go} | 30 +-- pkg/build/publish/kaniko_publisher.go | 225 +++++++++++++++++++++ pkg/build/publish/s2i_publisher.go | 97 +-------- pkg/client/cmd/install.go | 4 +- pkg/install/common.go | 11 +- pkg/install/operator.go | 54 ++++- pkg/platform/build.go | 16 +- pkg/stub/action/platform/initialize.go | 23 +-- pkg/util/minishift/minishift.go | 60 ++++++ .../operator.go => util/openshift/openshift.go} | 37 ++-- 19 files changed, 729 insertions(+), 207 deletions(-) diff --git a/deploy/builder-pvc.yaml b/deploy/builder-pvc.yaml new file mode 100644 index 0000000..8e6ca4b --- /dev/null +++ b/deploy/builder-pvc.yaml @@ -0,0 +1,12 @@ +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: camel-k-builder + labels: + app: "camel-k" +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Gi diff --git a/deploy/operator-deployment.yaml b/deploy/operator-deployment.yaml index e7a5180..c6e259f 100644 --- a/deploy/operator-deployment.yaml +++ b/deploy/operator-deployment.yaml @@ -33,3 +33,10 @@ spec: fieldPath: metadata.namespace - name: OPERATOR_NAME value: "camel-k-operator" + volumeMounts: + - mountPath: /workspace + name: camel-k-builder + volumes: + - name: camel-k-builder + persistentVolumeClaim: + claimName: camel-k-builder diff --git a/deploy/resources.go b/deploy/resources.go index a3274cb..39ab8ee 100644 --- a/deploy/resources.go +++ b/deploy/resources.go @@ -24,6 +24,22 @@ var Resources map[string]string func init() { Resources = make(map[string]string) + Resources["builder-pvc.yaml"] = + ` +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: camel-k-builder + labels: + app: "camel-k" +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Gi + +` Resources["camel-catalog.yaml"] = ` artifacts: @@ -2227,6 +2243,13 @@ spec: fieldPath: metadata.namespace - name: OPERATOR_NAME value: "camel-k-operator" + volumeMounts: + - mountPath: /workspace + name: camel-k-builder + volumes: + - name: camel-k-builder + persistentVolumeClaim: + claimName: camel-k-builder ` Resources["operator-role-binding.yaml"] = diff --git a/pkg/build/build_manager.go b/pkg/build/build_manager.go index 895575b..83c0f8a 100644 --- a/pkg/build/build_manager.go +++ b/pkg/build/build_manager.go @@ -28,14 +28,16 @@ type Manager struct { ctx context.Context builds sync.Map assembler Assembler + packager Packager publisher Publisher } -// NewManager creates an instance of the build manager using the given builder -func NewManager(ctx context.Context, assembler Assembler, publisher Publisher) *Manager { +// NewManager creates an instance of the build manager using the given assembler, packager and publisher +func NewManager(ctx context.Context, assembler Assembler, packager Packager, publisher Publisher) *Manager { return &Manager{ ctx: ctx, assembler: assembler, + packager: packager, publisher: publisher, } } @@ -68,7 +70,21 @@ func (m *Manager) Start(request Request) { } } - publishChannel := m.publisher.Publish(request, assembled) + packageChannel := m.packager.Package(request, assembled) + var packaged PackagedOutput + select { + case <-m.ctx.Done(): + m.builds.Store(request.Identifier, canceledBuildInfo(request)) + return + case packaged = <-packageChannel: + if packaged.Error != nil { + m.builds.Store(request.Identifier, failedPackageBuildInfo(request, packaged)) + return + } + } + defer m.packager.Cleanup(packaged) + + publishChannel := m.publisher.Publish(request, assembled, packaged) var published PublishedOutput select { case <-m.ctx.Done(): @@ -114,6 +130,14 @@ func failedAssembleBuildInfo(request Request, output AssembledOutput) Result { } } +func failedPackageBuildInfo(request Request, output PackagedOutput) Result { + return Result{ + Request: request, + Error: output.Error, + Status: StatusError, + } +} + func failedPublishBuildInfo(request Request, output PublishedOutput) Result { return Result{ Request: request, diff --git a/pkg/build/build_types.go b/pkg/build/build_types.go index 22ff178..bf83245 100644 --- a/pkg/build/build_types.go +++ b/pkg/build/build_types.go @@ -63,6 +63,19 @@ type Assembler interface { Assemble(Request) <-chan AssembledOutput } +// PackagedOutput is the new image layer that needs to be pushed +type PackagedOutput struct { + Error error + BaseImage string + TarFile string +} + +// A Packager produces the image layer that needs to be pushed +type Packager interface { + Package(Request, AssembledOutput) <-chan PackagedOutput + Cleanup(PackagedOutput) +} + // PublishedOutput is the output of the publish phase type PublishedOutput struct { Error error @@ -71,7 +84,7 @@ type PublishedOutput struct { // A Publisher publishes a docker image of a build request type Publisher interface { - Publish(Request, AssembledOutput) <-chan PublishedOutput + Publish(Request, AssembledOutput, PackagedOutput) <-chan PublishedOutput } // Status -- diff --git a/pkg/build/packager/base.go b/pkg/build/packager/base.go new file mode 100644 index 0000000..38cfdc7 --- /dev/null +++ b/pkg/build/packager/base.go @@ -0,0 +1,172 @@ +/* +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 packager + +import ( + "context" + "io/ioutil" + "os" + "path" + "time" + + "github.com/apache/camel-k/pkg/build" + "github.com/apache/camel-k/pkg/util/maven" + "github.com/apache/camel-k/pkg/util/tar" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +const ( + sharedDir = "/workspace" + artifactDirPrefix = "layer-" +) + +type commonPackager struct { + buffer chan packageOperation + uploadedArtifactsSelector +} + +type packageOperation struct { + request build.Request + assembled build.AssembledOutput + output chan build.PackagedOutput +} + +type uploadedArtifactsSelector func([]build.ClasspathEntry) (string, []build.ClasspathEntry, error) + +func newBasePackager(ctx context.Context, rootImage string) build.Packager { + identitySelector := func(entries []build.ClasspathEntry) (string, []build.ClasspathEntry, error) { + return rootImage, entries, nil + } + return newBasePackagerWithSelector(ctx, identitySelector) +} + +func newBasePackagerWithSelector(ctx context.Context, uploadedArtifactsSelector uploadedArtifactsSelector) *commonPackager { + pack := commonPackager{ + buffer: make(chan packageOperation, 100), + uploadedArtifactsSelector: uploadedArtifactsSelector, + } + go pack.packageCycle(ctx) + return &pack +} + +func (b *commonPackager) Package(request build.Request, assembled build.AssembledOutput) <-chan build.PackagedOutput { + res := make(chan build.PackagedOutput, 1) + op := packageOperation{ + request: request, + assembled: assembled, + output: res, + } + b.buffer <- op + return res +} + +func (b *commonPackager) Cleanup(output build.PackagedOutput) { + parentDir, _ := path.Split(output.TarFile) + err := os.RemoveAll(parentDir) + if err != nil { + logrus.Warn("Could not remove temporary directory ", parentDir) + } +} + +func (b *commonPackager) packageCycle(ctx context.Context) { + for { + select { + case <-ctx.Done(): + b.buffer = nil + return + case op := <-b.buffer: + now := time.Now() + logrus.Info("Starting a new image packaging") + res := b.execute(op.request, op.assembled) + elapsed := time.Now().Sub(now) + + if res.Error != nil { + logrus.Error("Error during packaging (total time ", elapsed.Seconds(), " seconds): ", res.Error) + } else { + logrus.Info("Packaging completed in ", elapsed.Seconds(), " seconds") + } + + op.output <- res + } + } +} + +func (b *commonPackager) execute(request build.Request, assembled build.AssembledOutput) build.PackagedOutput { + baseImageName, selectedArtifacts, err := b.uploadedArtifactsSelector(assembled.Classpath) + if err != nil { + return build.PackagedOutput{Error: err} + } + + tarFile, err := b.createTar(assembled, selectedArtifacts) + if err != nil { + return build.PackagedOutput{Error: err} + } + + return build.PackagedOutput{ + BaseImage: baseImageName, + TarFile: tarFile, + } +} + +func (b *commonPackager) createTar(assembled build.AssembledOutput, selectedArtifacts []build.ClasspathEntry) (string, error) { + artifactDir, err := ioutil.TempDir(sharedDir, artifactDirPrefix) + if err != nil { + return "", errors.Wrap(err, "could not create temporary dir for packaged artifacts") + } + + tarFileName := path.Join(artifactDir, "occi.tar") + tarAppender, err := tar.NewAppender(tarFileName) + if err != nil { + return "", err + } + defer tarAppender.Close() + + tarDir := "dependencies/" + for _, entry := range selectedArtifacts { + gav, err := maven.ParseGAV(entry.ID) + if err != nil { + return "", nil + } + + tarPath := path.Join(tarDir, gav.GroupID) + _, err = tarAppender.AddFile(entry.Location, tarPath) + if err != nil { + return "", err + } + } + + cp := "" + for _, entry := range assembled.Classpath { + gav, err := maven.ParseGAV(entry.ID) + if err != nil { + return "", nil + } + tarPath := path.Join(tarDir, gav.GroupID) + _, fileName := path.Split(entry.Location) + fileName = path.Join(tarPath, fileName) + cp += fileName + "\n" + } + + err = tarAppender.AppendData([]byte(cp), "classpath") + if err != nil { + return "", err + } + + return tarFileName, nil +} diff --git a/pkg/install/operator.go b/pkg/build/packager/doc.go similarity index 57% copy from pkg/install/operator.go copy to pkg/build/packager/doc.go index eb3ac4c..cfa21c6 100644 --- a/pkg/install/operator.go +++ b/pkg/build/packager/doc.go @@ -15,27 +15,5 @@ See the License for the specific language governing permissions and limitations under the License. */ -package install - -// Operator -- -func Operator(namespace string) error { - return Resources(namespace, - "operator-service-account.yaml", - "operator-role-openshift.yaml", // TODO distinguish between Openshift and Kubernetes - "operator-role-binding.yaml", - "operator-deployment.yaml", - "operator-service.yaml", - ) -} - -// Platform installs the platform custom resource -func Platform(namespace string) error { - return Resource(namespace, "platform-cr.yaml") -} - -// Example -- -func Example(namespace string) error { - return Resources(namespace, - "cr-example.yaml", - ) -} +// Package packager contains strategies for building layers of a Docker image +package packager diff --git a/pkg/build/packager/factory.go b/pkg/build/packager/factory.go new file mode 100644 index 0000000..48ad665 --- /dev/null +++ b/pkg/build/packager/factory.go @@ -0,0 +1,49 @@ +/* +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 packager + +import ( + "context" + "github.com/apache/camel-k/pkg/build" +) + +const ( + s2iRootImage = "fabric8/s2i-java:2.3" + //javaRootImage = "fabric8/java-jboss-openjdk8-jdk:1.5.1" + javaRootImage = "fabric8/java-alpine-openjdk8-jdk:1.5.1" +) + +// NewS2IStandardPackager creates a standard packager for S2I builds +func NewS2IStandardPackager(ctx context.Context) build.Packager { + return newBasePackager(ctx, s2iRootImage) +} + +// NewS2IIncrementalPackager creates a incremental packager for S2I builds +func NewS2IIncrementalPackager(ctx context.Context, lister PublishedImagesLister) build.Packager { + return newIncrementalPackager(ctx, lister, s2iRootImage) +} + +// NewJavaStandardPackager creates a standard packager for Java Docker builds +func NewJavaStandardPackager(ctx context.Context) build.Packager { + return newBasePackager(ctx, javaRootImage) +} + +// NewJavaIncrementalPackager creates a incremental packager for Java Docker builds +func NewJavaIncrementalPackager(ctx context.Context, lister PublishedImagesLister) build.Packager { + return newIncrementalPackager(ctx, lister, javaRootImage) +} diff --git a/pkg/build/publish/s2i_incremental_publisher.go b/pkg/build/packager/incremental.go similarity index 62% rename from pkg/build/publish/s2i_incremental_publisher.go rename to pkg/build/packager/incremental.go index ae996e4..cb9334b 100644 --- a/pkg/build/publish/s2i_incremental_publisher.go +++ b/pkg/build/packager/incremental.go @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package publish +package packager import ( "context" @@ -23,36 +23,31 @@ import ( "github.com/apache/camel-k/pkg/build" ) -type s2iIncrementalPublisher struct { - s2iPublisher *s2iPublisher - lister PublishedImagesLister +type incrementalPackager struct { + commonPackager *commonPackager + lister PublishedImagesLister + rootImage string } -// PublishedImage represent a base image that can be used as starting point -type PublishedImage struct { - Image string - Classpath []string -} - -// PublishedImagesLister allows to list all images already published -type PublishedImagesLister interface { - ListPublishedImages() ([]PublishedImage, error) +// newIncrementalPackager creates a new packager that is able to create a layer on top of a existing image +func newIncrementalPackager(ctx context.Context, lister PublishedImagesLister, rootImage string) build.Packager { + layeredPackager := incrementalPackager{ + lister: lister, + rootImage: rootImage, + } + layeredPackager.commonPackager = newBasePackagerWithSelector(ctx, layeredPackager.selectArtifactsToUpload) + return &layeredPackager } -// NewS2IIncrementalPublisher creates a new publisher that is able to do a Openshift S2I binary builds on top of other builds -func NewS2IIncrementalPublisher(ctx context.Context, namespace string, lister PublishedImagesLister) build.Publisher { - layeredPublisher := s2iIncrementalPublisher{ - lister: lister, - } - layeredPublisher.s2iPublisher = newS2IPublisher(ctx, namespace, layeredPublisher.selectArtifactsToUpload) - return &layeredPublisher +func (p *incrementalPackager) Package(req build.Request, assembled build.AssembledOutput) <-chan build.PackagedOutput { + return p.commonPackager.Package(req, assembled) } -func (p *s2iIncrementalPublisher) Publish(req build.Request, assembled build.AssembledOutput) <-chan build.PublishedOutput { - return p.s2iPublisher.Publish(req, assembled) +func (p *incrementalPackager) Cleanup(output build.PackagedOutput) { + p.commonPackager.Cleanup(output) } -func (p *s2iIncrementalPublisher) selectArtifactsToUpload(entries []build.ClasspathEntry) (string, []build.ClasspathEntry, error) { +func (p *incrementalPackager) selectArtifactsToUpload(entries []build.ClasspathEntry) (string, []build.ClasspathEntry, error) { images, err := p.lister.ListPublishedImages() if err != nil { return "", nil, err @@ -71,10 +66,10 @@ func (p *s2iIncrementalPublisher) selectArtifactsToUpload(entries []build.Classp } // return default selection - return baseImage, entries, nil + return p.rootImage, entries, nil } -func (p *s2iIncrementalPublisher) findBestImage(images []PublishedImage, entries []build.ClasspathEntry) (*PublishedImage, map[string]bool) { +func (p *incrementalPackager) findBestImage(images []PublishedImage, entries []build.ClasspathEntry) (*PublishedImage, map[string]bool) { if len(images) == 0 { return nil, nil } diff --git a/pkg/install/operator.go b/pkg/build/packager/types.go similarity index 57% copy from pkg/install/operator.go copy to pkg/build/packager/types.go index eb3ac4c..4965e28 100644 --- a/pkg/install/operator.go +++ b/pkg/build/packager/types.go @@ -15,27 +15,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package install +package packager -// Operator -- -func Operator(namespace string) error { - return Resources(namespace, - "operator-service-account.yaml", - "operator-role-openshift.yaml", // TODO distinguish between Openshift and Kubernetes - "operator-role-binding.yaml", - "operator-deployment.yaml", - "operator-service.yaml", - ) +// PublishedImage represent a base image that can be used as starting point +type PublishedImage struct { + Image string + Classpath []string } -// Platform installs the platform custom resource -func Platform(namespace string) error { - return Resource(namespace, "platform-cr.yaml") -} - -// Example -- -func Example(namespace string) error { - return Resources(namespace, - "cr-example.yaml", - ) -} +// PublishedImagesLister allows to list all images already published +type PublishedImagesLister interface { + ListPublishedImages() ([]PublishedImage, error) +} \ No newline at end of file diff --git a/pkg/build/publish/kaniko_publisher.go b/pkg/build/publish/kaniko_publisher.go new file mode 100644 index 0000000..7c5a511 --- /dev/null +++ b/pkg/build/publish/kaniko_publisher.go @@ -0,0 +1,225 @@ +/* +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 publish + +import ( + tarutils "archive/tar" + "context" + "github.com/apache/camel-k/pkg/util/kubernetes" + "io" + "io/ioutil" + "os" + "path" + "time" + + "github.com/apache/camel-k/pkg/build" + "github.com/operator-framework/operator-sdk/pkg/sdk" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type kanikoPublisher struct { + buffer chan kanikoPublishOperation + namespace string + registry string +} + +type kanikoPublishOperation struct { + request build.Request + assembled build.AssembledOutput + packaged build.PackagedOutput + output chan build.PublishedOutput +} + +// NewKanikoPublisher creates a new publisher doing a Kaniko image push +func NewKanikoPublisher(ctx context.Context, namespace string, registry string) build.Publisher { + publisher := kanikoPublisher{ + buffer: make(chan kanikoPublishOperation, 100), + namespace: namespace, + registry: registry, + } + go publisher.publishCycle(ctx) + return &publisher +} + +func (b *kanikoPublisher) Publish(request build.Request, assembled build.AssembledOutput, packaged build.PackagedOutput) <-chan build.PublishedOutput { + res := make(chan build.PublishedOutput, 1) + op := kanikoPublishOperation{ + request: request, + assembled: assembled, + packaged: packaged, + output: res, + } + b.buffer <- op + return res +} + +func (b *kanikoPublisher) publishCycle(ctx context.Context) { + for { + select { + case <-ctx.Done(): + b.buffer = nil + return + case op := <-b.buffer: + now := time.Now() + logrus.Info("Starting a new image publication") + res := b.execute(op.request, op.assembled, op.packaged) + elapsed := time.Now().Sub(now) + + if res.Error != nil { + logrus.Error("Error during publication (total time ", elapsed.Seconds(), " seconds): ", res.Error) + } else { + logrus.Info("Publication completed in ", elapsed.Seconds(), " seconds") + } + + op.output <- res + } + } +} + +func (b *kanikoPublisher) execute(request build.Request, assembled build.AssembledOutput, packaged build.PackagedOutput) build.PublishedOutput { + image, err := b.publish(packaged.TarFile, packaged.BaseImage, request) + if err != nil { + return build.PublishedOutput{Error: err} + } + + return build.PublishedOutput{Image: image} +} + +func (b *kanikoPublisher) publish(tarFile string, baseImageName string, source build.Request) (string, error) { + image := b.registry + "/" + b.namespace + "/camel-k-" + source.Identifier.Name + ":" + source.Identifier.Qualifier + contextDir, err := b.prepareContext(tarFile, baseImageName) + if err != nil { + return "", err + } + pod := v1.Pod{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1.SchemeGroupVersion.String(), + Kind: "Pod", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: b.namespace, + Name: "camel-k-" + source.Identifier.Name, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "kaniko", + Image: "gcr.io/kaniko-project/executor:latest", + Args: []string{ + "--dockerfile=Dockerfile", + "--context=" + contextDir, + "--destination=" + image, + "--insecure", + }, + VolumeMounts: []v1.VolumeMount{ + { + Name: "camel-k-builder", + MountPath: "/workspace", + }, + }, + }, + }, + RestartPolicy: v1.RestartPolicyNever, + Volumes: []v1.Volume{ + { + Name: "camel-k-builder", + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: "camel-k-builder", + ReadOnly: true, + }, + }, + }, + }, + }, + } + + sdk.Delete(&pod) + err = sdk.Create(&pod) + if err != nil { + return "", errors.Wrap(err, "cannot create kaniko builder pod") + } + + err = kubernetes.WaitCondition(&pod, func(obj interface{}) (bool, error) { + if val, ok := obj.(*v1.Pod); ok { + if val.Status.Phase == v1.PodSucceeded { + return true, nil + } else if val.Status.Phase == v1.PodFailed { + return false, errors.New("build failed") + } + } + return false, nil + }, 5*time.Minute) + + if err != nil { + return "", err + } + + return image, nil +} + +func (b *kanikoPublisher) prepareContext(tarName string, baseImage string) (string, error) { + baseDir, _ := path.Split(tarName) + contextDir := path.Join(baseDir, "context") + if err := b.unTar(tarName, contextDir); err != nil { + return "", err + } + + dockerFileContent := []byte(` + FROM ` + baseImage + ` + ADD . /deployments + `) + if err := ioutil.WriteFile(path.Join(contextDir, "Dockerfile"), dockerFileContent, 0777); err != nil { + return "", err + } + return contextDir, nil +} + +func (b *kanikoPublisher) unTar(tarName string, dir string) error { + file, err := os.Open(tarName) + if err != nil { + return err + } + defer file.Close() + reader := tarutils.NewReader(file) + for { + header, err := reader.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + targetName := path.Join(dir, header.Name) + targetDir, _ := path.Split(targetName) + if err := os.MkdirAll(targetDir, 0777); err != nil { + return err + } + buffer, err := ioutil.ReadAll(reader) + if err != nil { + return err + } + if err := ioutil.WriteFile(targetName, buffer, os.FileMode(header.Mode)); err != nil { + return err + } + } + return nil +} diff --git a/pkg/build/publish/s2i_publisher.go b/pkg/build/publish/s2i_publisher.go index 97147a7..09a0105 100644 --- a/pkg/build/publish/s2i_publisher.go +++ b/pkg/build/publish/s2i_publisher.go @@ -20,14 +20,11 @@ package publish import ( "context" "io/ioutil" - "path" "time" "github.com/apache/camel-k/pkg/build" "github.com/apache/camel-k/pkg/util/kubernetes" "github.com/apache/camel-k/pkg/util/kubernetes/customclient" - "github.com/apache/camel-k/pkg/util/maven" - "github.com/apache/camel-k/pkg/util/tar" buildv1 "github.com/openshift/api/build/v1" imagev1 "github.com/openshift/api/image/v1" "github.com/operator-framework/operator-sdk/pkg/sdk" @@ -39,49 +36,34 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -const ( - artifactDirPrefix = "s2i-" - baseImage = "fabric8/s2i-java:2.3" -) - type s2iPublisher struct { - buffer chan publishOperation + buffer chan s2iPublishOperation namespace string - uploadedArtifactsSelector } -type publishOperation struct { +type s2iPublishOperation struct { request build.Request assembled build.AssembledOutput + packaged build.PackagedOutput output chan build.PublishedOutput } -type uploadedArtifactsSelector func([]build.ClasspathEntry) (string, []build.ClasspathEntry, error) - // NewS2IPublisher creates a new publisher doing a Openshift S2I binary build func NewS2IPublisher(ctx context.Context, namespace string) build.Publisher { - identitySelector := func(entries []build.ClasspathEntry) (string, []build.ClasspathEntry, error) { - return baseImage, entries, nil - } - return newS2IPublisher(ctx, namespace, identitySelector) -} - -// NewS2IPublisher creates a new publisher doing a Openshift S2I binary build -func newS2IPublisher(ctx context.Context, namespace string, uploadedArtifactsSelector uploadedArtifactsSelector) *s2iPublisher { publisher := s2iPublisher{ - buffer: make(chan publishOperation, 100), - namespace: namespace, - uploadedArtifactsSelector: uploadedArtifactsSelector, + buffer: make(chan s2iPublishOperation, 100), + namespace: namespace, } go publisher.publishCycle(ctx) return &publisher } -func (b *s2iPublisher) Publish(request build.Request, assembled build.AssembledOutput) <-chan build.PublishedOutput { +func (b *s2iPublisher) Publish(request build.Request, assembled build.AssembledOutput, packaged build.PackagedOutput) <-chan build.PublishedOutput { res := make(chan build.PublishedOutput, 1) - op := publishOperation{ + op := s2iPublishOperation{ request: request, assembled: assembled, + packaged: packaged, output: res, } b.buffer <- op @@ -97,7 +79,7 @@ func (b *s2iPublisher) publishCycle(ctx context.Context) { case op := <-b.buffer: now := time.Now() logrus.Info("Starting a new image publication") - res := b.execute(op.request, op.assembled) + res := b.execute(op.request, op.assembled, op.packaged) elapsed := time.Now().Sub(now) if res.Error != nil { @@ -111,18 +93,8 @@ func (b *s2iPublisher) publishCycle(ctx context.Context) { } } -func (b *s2iPublisher) execute(request build.Request, assembled build.AssembledOutput) build.PublishedOutput { - baseImageName, selectedArtifacts, err := b.uploadedArtifactsSelector(assembled.Classpath) - if err != nil { - return build.PublishedOutput{Error: err} - } - - tarFile, err := b.createTar(assembled, selectedArtifacts) - if err != nil { - return build.PublishedOutput{Error: err} - } - - image, err := b.publish(tarFile, baseImageName, request) +func (b *s2iPublisher) execute(request build.Request, assembled build.AssembledOutput, packaged build.PackagedOutput) build.PublishedOutput { + image, err := b.publish(packaged.TarFile, packaged.BaseImage, request) if err != nil { return build.PublishedOutput{Error: err} } @@ -253,50 +225,3 @@ func (b *s2iPublisher) publish(tarFile string, imageName string, source build.Re } return is.Status.DockerImageRepository + ":" + source.Identifier.Qualifier, nil } - -func (b *s2iPublisher) createTar(assembled build.AssembledOutput, selectedArtifacts []build.ClasspathEntry) (string, error) { - artifactDir, err := ioutil.TempDir("", artifactDirPrefix) - if err != nil { - return "", errors.Wrap(err, "could not create temporary dir for s2i artifacts") - } - - tarFileName := path.Join(artifactDir, "occi.tar") - tarAppender, err := tar.NewAppender(tarFileName) - if err != nil { - return "", err - } - defer tarAppender.Close() - - tarDir := "dependencies/" - for _, entry := range selectedArtifacts { - gav, err := maven.ParseGAV(entry.ID) - if err != nil { - return "", nil - } - - tarPath := path.Join(tarDir, gav.GroupID) - _, err = tarAppender.AddFile(entry.Location, tarPath) - if err != nil { - return "", err - } - } - - cp := "" - for _, entry := range assembled.Classpath { - gav, err := maven.ParseGAV(entry.ID) - if err != nil { - return "", nil - } - tarPath := path.Join(tarDir, gav.GroupID) - _, fileName := path.Split(entry.Location) - fileName = path.Join(tarPath, fileName) - cp += fileName + "\n" - } - - err = tarAppender.AppendData([]byte(cp), "classpath") - if err != nil { - return "", err - } - - return tarFileName, nil -} diff --git a/pkg/client/cmd/install.go b/pkg/client/cmd/install.go index 0d20021..abe61c5 100644 --- a/pkg/client/cmd/install.go +++ b/pkg/client/cmd/install.go @@ -41,6 +41,7 @@ func newCmdInstall(rootCmdOptions *RootCmdOptions) *cobra.Command { cmd.Flags().BoolVar(&options.clusterSetupOnly, "cluster-setup", false, "Execute cluster-wide operations only (may require admin rights)") cmd.Flags().BoolVar(&options.exampleSetup, "example", false, "Install example integration") + cmd.Flags().StringVar(&options.registry, "registry", "", "A Docker registry that can be used to publish images") cmd.ParseFlags(os.Args) return &cmd @@ -50,6 +51,7 @@ type installCmdOptions struct { *RootCmdOptions clusterSetupOnly bool exampleSetup bool + registry string } func (o *installCmdOptions) install(cmd *cobra.Command, args []string) error { @@ -69,7 +71,7 @@ func (o *installCmdOptions) install(cmd *cobra.Command, args []string) error { return err } - err = install.Platform(namespace) + err = install.Platform(namespace, o.registry) if err != nil { return err } diff --git a/pkg/install/common.go b/pkg/install/common.go index bef7f6f..6d7c61f 100644 --- a/pkg/install/common.go +++ b/pkg/install/common.go @@ -24,6 +24,7 @@ import ( "github.com/operator-framework/operator-sdk/pkg/sdk" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" ) // Resources installs named resources from the project resource directory @@ -43,11 +44,16 @@ func Resource(namespace string, name string) error { return err } + return RuntimeObject(namespace, obj) +} + +// RuntimeObject installs a single runtime object +func RuntimeObject(namespace string, obj runtime.Object) error { if metaObject, ok := obj.(metav1.Object); ok { metaObject.SetNamespace(namespace) } - err = sdk.Create(obj) + err := sdk.Create(obj) if err != nil && errors.IsAlreadyExists(err) { // Don't recreate Service object if obj.GetObjectKind().GroupVersionKind().Kind == "Service" { @@ -60,6 +66,9 @@ func Resource(namespace string, name string) error { if obj.GetObjectKind().GroupVersionKind().Kind == v1alpha1.IntegrationPlatformKind { return nil } + if obj.GetObjectKind().GroupVersionKind().Kind == "PersistentVolumeClaim" { + return nil + } return sdk.Update(obj) } return err diff --git a/pkg/install/operator.go b/pkg/install/operator.go index eb3ac4c..f0ec970 100644 --- a/pkg/install/operator.go +++ b/pkg/install/operator.go @@ -17,20 +17,68 @@ limitations under the License. package install +import ( + "errors" + "github.com/apache/camel-k/deploy" + "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" + "github.com/apache/camel-k/pkg/util/kubernetes" + "github.com/apache/camel-k/pkg/util/minishift" + "github.com/apache/camel-k/pkg/util/openshift" +) + // Operator -- func Operator(namespace string) error { + isOpenshift, err := openshift.IsOpenShift() + if err != nil { + return err + } + var operatorRole string + if isOpenshift { + operatorRole = "operator-role-openshift.yaml" + } else { + operatorRole = "operator-role-kubernetes.yaml" + } return Resources(namespace, "operator-service-account.yaml", - "operator-role-openshift.yaml", // TODO distinguish between Openshift and Kubernetes + operatorRole, "operator-role-binding.yaml", + "builder-pvc.yaml", "operator-deployment.yaml", "operator-service.yaml", ) } // Platform installs the platform custom resource -func Platform(namespace string) error { - return Resource(namespace, "platform-cr.yaml") +func Platform(namespace string, registry string) error { + isOpenshift, err := openshift.IsOpenShift() + if err != nil { + return err + } + if isOpenshift { + return Resource(namespace, "platform-cr.yaml") + } + platform, err := kubernetes.LoadResourceFromYaml(deploy.Resources["platform-cr.yaml"]) + if err != nil { + return err + } + if pl, ok := platform.(*v1alpha1.IntegrationPlatform); !ok { + panic("cannot find integration platform template") + } else { + if registry == "" { + // This operation should be done here in the installer + // because the operator is not allowed to look into the "kube-system" namespace + minishiftRegistry, err := minishift.FindRegistry() + if err != nil { + return err + } + if minishiftRegistry == nil { + return errors.New("cannot find automatically a registry where to push images") + } + registry = *minishiftRegistry + } + pl.Spec.Build.Registry = registry + return RuntimeObject(namespace, pl) + } } // Example -- diff --git a/pkg/platform/build.go b/pkg/platform/build.go index 994ced6..6e75ad2 100644 --- a/pkg/platform/build.go +++ b/pkg/platform/build.go @@ -23,6 +23,7 @@ import ( "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" "github.com/apache/camel-k/pkg/build" "github.com/apache/camel-k/pkg/build/assemble" + "github.com/apache/camel-k/pkg/build/packager" "github.com/apache/camel-k/pkg/build/publish" "github.com/operator-framework/operator-sdk/pkg/sdk" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -44,8 +45,13 @@ func GetPlatformBuildManager(ctx context.Context, namespace string) (*build.Mana assembler := assemble.NewMavenAssembler(ctx) if pl.Spec.Build.PublishStrategy == v1alpha1.IntegrationPlatformBuildPublishStrategyS2I { - publisher := publish.NewS2IIncrementalPublisher(ctx, namespace, newContextLister(namespace)) - buildManager = build.NewManager(ctx, assembler, publisher) + packaging := packager.NewS2IIncrementalPackager(ctx, newContextLister(namespace)) + publisher := publish.NewS2IPublisher(ctx, namespace) + buildManager = build.NewManager(ctx, assembler, packaging, publisher) + } else if pl.Spec.Build.PublishStrategy == v1alpha1.IntegrationPlatformBuildPublishStrategyKaniko && pl.Spec.Build.Registry != "" { + packaging := packager.NewJavaStandardPackager(ctx) + publisher := publish.NewKanikoPublisher(ctx, namespace, pl.Spec.Build.Registry) + buildManager = build.NewManager(ctx, assembler, packaging, publisher) } if buildManager == nil { @@ -66,14 +72,14 @@ func newContextLister(namespace string) contextLister { } } -func (l contextLister) ListPublishedImages() ([]publish.PublishedImage, error) { +func (l contextLister) ListPublishedImages() ([]packager.PublishedImage, error) { list := v1alpha1.NewIntegrationContextList() err := sdk.List(l.namespace, &list, sdk.WithListOptions(&metav1.ListOptions{})) if err != nil { return nil, err } - images := make([]publish.PublishedImage, 0) + images := make([]packager.PublishedImage, 0) for _, ctx := range list.Items { if ctx.Status.Phase != v1alpha1.IntegrationContextPhaseReady || ctx.Labels == nil { continue @@ -82,7 +88,7 @@ func (l contextLister) ListPublishedImages() ([]publish.PublishedImage, error) { continue } - images = append(images, publish.PublishedImage{ + images = append(images, packager.PublishedImage{ Image: ctx.Status.Image, Classpath: ctx.Status.Classpath, }) diff --git a/pkg/stub/action/platform/initialize.go b/pkg/stub/action/platform/initialize.go index 4215daa..07ea31f 100644 --- a/pkg/stub/action/platform/initialize.go +++ b/pkg/stub/action/platform/initialize.go @@ -18,12 +18,12 @@ limitations under the License. package platform import ( + "errors" "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" platformutils "github.com/apache/camel-k/pkg/platform" - "github.com/operator-framework/operator-sdk/pkg/k8sclient" + "github.com/apache/camel-k/pkg/util/openshift" "github.com/operator-framework/operator-sdk/pkg/sdk" "github.com/sirupsen/logrus" - "k8s.io/apimachinery/pkg/api/errors" ) // NewInitializeAction returns a action that initializes the platform configuration when not provided by the user @@ -63,9 +63,9 @@ func (action *initializeAction) Handle(platform *v1alpha1.IntegrationPlatform) e // update missing fields in the resource if target.Spec.Cluster == "" { // determine the kind of cluster the platform in installed into - if openshift, err := action.isOpenshift(); err != nil { + if isOpenshift, err := openshift.IsOpenShift(); err != nil { return err - } else if openshift { + } else if isOpenshift { target.Spec.Cluster = v1alpha1.IntegrationPlatformClusterOpenShift } else { target.Spec.Cluster = v1alpha1.IntegrationPlatformClusterKubernetes @@ -77,26 +77,19 @@ func (action *initializeAction) Handle(platform *v1alpha1.IntegrationPlatform) e target.Spec.Build.PublishStrategy = v1alpha1.IntegrationPlatformBuildPublishStrategyS2I } else { target.Spec.Build.PublishStrategy = v1alpha1.IntegrationPlatformBuildPublishStrategyKaniko - // TODO discover registry location } } + if target.Spec.Build.PublishStrategy == v1alpha1.IntegrationPlatformBuildPublishStrategyKaniko && target.Spec.Build.Registry == "" { + return errors.New("no registry specified for publishing images") + } + // next status logrus.Info("Platform ", target.Name, " transitioning to state ", v1alpha1.IntegrationPlatformPhaseCreating) target.Status.Phase = v1alpha1.IntegrationPlatformPhaseCreating return sdk.Update(target) } -func (action *initializeAction) isOpenshift() (bool, error) { - _, err := k8sclient.GetKubeClient().Discovery().ServerResourcesForGroupVersion("image.openshift.io/v1") - if err != nil && errors.IsNotFound(err) { - return false, nil - } else if err != nil { - return false, err - } - return true, nil -} - func (action *initializeAction) isDuplicate(thisPlatform *v1alpha1.IntegrationPlatform) (bool, error) { platforms, err := platformutils.ListPlatforms(thisPlatform.Namespace) if err != nil { diff --git a/pkg/util/minishift/minishift.go b/pkg/util/minishift/minishift.go new file mode 100644 index 0000000..9e5465c --- /dev/null +++ b/pkg/util/minishift/minishift.go @@ -0,0 +1,60 @@ +/* +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 minishift contains utilities for Minishift deployments +package minishift + +import ( + "github.com/operator-framework/operator-sdk/pkg/sdk" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "strconv" +) + +const ( + registryNamespace = "kube-system" +) + +// FindRegistry returns the Minishift registry location if any +func FindRegistry() (*string, error) { + svcs := v1.ServiceList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1.SchemeGroupVersion.String(), + Kind: "Service", + }, + } + options := metav1.ListOptions{ + LabelSelector: "kubernetes.io/minikube-addons=registry", + } + if err := sdk.List(registryNamespace, &svcs, sdk.WithListOptions(&options)); err != nil { + return nil, err + } + if len(svcs.Items) == 0 { + return nil, nil + } + svc := svcs.Items[0] + ip := svc.Spec.ClusterIP + portStr := "" + if len(svc.Spec.Ports) > 0 { + port := svc.Spec.Ports[0].Port + if port > 0 && port != 80 { + portStr = ":" + strconv.FormatInt(int64(port), 10) + } + } + registry := ip + portStr + return ®istry, nil +} diff --git a/pkg/install/operator.go b/pkg/util/openshift/openshift.go similarity index 57% copy from pkg/install/operator.go copy to pkg/util/openshift/openshift.go index eb3ac4c..f5b69b0 100644 --- a/pkg/install/operator.go +++ b/pkg/util/openshift/openshift.go @@ -15,27 +15,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -package install +package openshift -// Operator -- -func Operator(namespace string) error { - return Resources(namespace, - "operator-service-account.yaml", - "operator-role-openshift.yaml", // TODO distinguish between Openshift and Kubernetes - "operator-role-binding.yaml", - "operator-deployment.yaml", - "operator-service.yaml", - ) -} +import ( + "github.com/operator-framework/operator-sdk/pkg/k8sclient" + "k8s.io/apimachinery/pkg/api/errors" +) -// Platform installs the platform custom resource -func Platform(namespace string) error { - return Resource(namespace, "platform-cr.yaml") -} - -// Example -- -func Example(namespace string) error { - return Resources(namespace, - "cr-example.yaml", - ) -} +// IsOpenShift returns true if we are connected to a OpenShift cluster +func IsOpenShift() (bool, error) { + _, err := k8sclient.GetKubeClient().Discovery().ServerResourcesForGroupVersion("image.openshift.io/v1") + if err != nil && errors.IsNotFound(err) { + return false, nil + } else if err != nil { + return false, err + } + return true, nil +} \ No newline at end of file
