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
The following commit(s) were added to refs/heads/master by this push: new 1e1565d add an option to always generate a docker image #246 1e1565d is described below commit 1e1565d776b0e93b5761fc6dfd9291a51ff047bc Author: lburgazzoli <lburgazz...@gmail.com> AuthorDate: Tue Dec 4 17:51:26 2018 +0100 add an option to always generate a docker image #246 --- Gopkg.lock | 9 + pkg/apis/camel/v1alpha1/types.go | 10 +- pkg/builder/builder.go | 9 + pkg/builder/builder_steps.go | 106 +++++--- pkg/builder/builder_types.go | 51 ++-- pkg/builder/builder_utils.go | 17 +- pkg/builder/springboot/initializer.go | 3 - pkg/stub/action/context/build.go | 12 +- .../integration/{build.go => build_context.go} | 45 ++-- pkg/stub/action/integration/build_image.go | 144 +++++++++++ pkg/stub/action/integration/initialize.go | 7 +- pkg/stub/action/integration/monitor.go | 6 +- pkg/stub/handler.go | 3 +- pkg/trait/builder.go | 38 ++- pkg/trait/catalog.go | 8 +- pkg/trait/deployment.go | 153 +++++++---- pkg/trait/types.go | 7 +- pkg/util/tar/appender.go | 14 +- test/build_manager_integration_test.go | 7 - test/testing_env.go | 16 -- vendor/github.com/scylladb/go-set/LICENSE | 177 +++++++++++++ vendor/github.com/scylladb/go-set/strset/strset.go | 279 +++++++++++++++++++++ 22 files changed, 958 insertions(+), 163 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 6e9a08d..a097acd 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -493,6 +493,14 @@ version = "v1.2.1" [[projects]] + digest = "1:6f3ce746342be7b14a2d1ca33a4a11fd6cb0300e5d34c766f01e19e936fb10af" + name = "github.com/scylladb/go-set" + packages = ["strset"] + pruneopts = "NUT" + revision = "e560bb8f49bb7f34d4f59b7e771f6e1307c329da" + version = "v1.0.2" + +[[projects]] digest = "1:ecf78eacf406c42f07f66d6b79fda24d2b92dc711bfd0760d0c931678f9621fe" name = "github.com/sirupsen/logrus" packages = ["."] @@ -935,6 +943,7 @@ "github.com/pkg/errors", "github.com/radovskyb/watcher", "github.com/rs/xid", + "github.com/scylladb/go-set/strset", "github.com/sirupsen/logrus", "github.com/spf13/cobra", "github.com/stoewer/go-strcase", diff --git a/pkg/apis/camel/v1alpha1/types.go b/pkg/apis/camel/v1alpha1/types.go index b549e1d..f2b506f 100644 --- a/pkg/apis/camel/v1alpha1/types.go +++ b/pkg/apis/camel/v1alpha1/types.go @@ -130,8 +130,10 @@ const ( // IntegrationKind -- IntegrationKind string = "Integration" - // IntegrationPhaseBuilding -- - IntegrationPhaseBuilding IntegrationPhase = "Building" + // IntegrationPhaseBuildingContext -- + IntegrationPhaseBuildingContext IntegrationPhase = "Building Context" + // IntegrationPhaseBuildingImage -- + IntegrationPhaseBuildingImage IntegrationPhase = "Building Image" // IntegrationPhaseDeploying -- IntegrationPhaseDeploying IntegrationPhase = "Deploying" // IntegrationPhaseRunning -- @@ -290,3 +292,7 @@ type Artifact struct { Location string `json:"location,omitempty" yaml:"location,omitempty"` Target string `json:"target,omitempty" yaml:"target,omitempty"` } + +func (in *Artifact) String() string { + return in.ID +} diff --git a/pkg/builder/builder.go b/pkg/builder/builder.go index db82150..f4dad8b 100644 --- a/pkg/builder/builder.go +++ b/pkg/builder/builder.go @@ -151,6 +151,10 @@ func (b *defaultBuilder) submit(request Request) { Image: "fabric8/s2i-java:2.3", // TODO: externalize } + if request.Image != "" { + c.Image = request.Image + } + // Sort steps by phase sort.SliceStable(request.Steps, func(i, j int) bool { return request.Steps[i].Phase() < request.Steps[j].Phase() @@ -201,4 +205,9 @@ func (b *defaultBuilder) submit(request Request) { b.request.Store(request.Meta.Name, r) b.log.Infof("request to build context %s executed in %f seconds", request.Meta.Name, r.Task.Elapsed().Seconds()) + b.log.Infof("dependencies : %s", request.Dependencies) + b.log.Infof("artifacts : %s", ArtifactIDs(c.Artifacts)) + b.log.Infof("artifacts selected : %s", ArtifactIDs(c.SelectedArtifacts)) + b.log.Infof("requested image : %s", request.Image) + b.log.Infof("resolved image : %s", c.Image) } diff --git a/pkg/builder/builder_steps.go b/pkg/builder/builder_steps.go index cac7e75..4a24e02 100644 --- a/pkg/builder/builder_steps.go +++ b/pkg/builder/builder_steps.go @@ -25,6 +25,8 @@ import ( "path" "strings" + "github.com/scylladb/go-set/strset" + "github.com/rs/xid" "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" @@ -171,49 +173,58 @@ func ComputeDependencies(ctx *Context) error { } // ArtifactsSelector -- -type ArtifactsSelector func([]v1alpha1.Artifact) (string, []v1alpha1.Artifact, error) +type ArtifactsSelector func(ctx *Context) error // StandardPackager -- func StandardPackager(ctx *Context) error { - return packager(ctx, func(libraries []v1alpha1.Artifact) (string, []v1alpha1.Artifact, error) { - return ctx.Image, libraries, nil + return packager(ctx, func(ctx *Context) error { + ctx.SelectedArtifacts = ctx.Artifacts + + return nil }) } // IncrementalPackager -- func IncrementalPackager(ctx *Context) error { + if ctx.HasRequiredImage() { + // + // If the build requires a specific image, don't try to determine the + // base image using artifact so just use the standard packages + // + return StandardPackager(ctx) + } + images, err := ListPublishedImages(ctx.Namespace) if err != nil { return err } - return packager(ctx, func(libraries []v1alpha1.Artifact) (string, []v1alpha1.Artifact, error) { - bestImage, commonLibs := FindBestImage(images, libraries) - if bestImage != nil { - selectedClasspath := make([]v1alpha1.Artifact, 0) - for _, entry := range libraries { + return packager(ctx, func(ctx *Context) error { + ctx.SelectedArtifacts = ctx.Artifacts + + bestImage, commonLibs := FindBestImage(images, ctx.Request.Dependencies, ctx.Artifacts) + if bestImage.Image != "" { + selectedArtifacts := make([]v1alpha1.Artifact, 0) + for _, entry := range ctx.Artifacts { if _, isCommon := commonLibs[entry.ID]; !isCommon { - selectedClasspath = append(selectedClasspath, entry) + selectedArtifacts = append(selectedArtifacts, entry) } } - return bestImage.Image, selectedClasspath, nil + ctx.Image = bestImage.Image + ctx.SelectedArtifacts = selectedArtifacts } - // return default selection - return ctx.Image, libraries, nil + return nil }) } // ClassPathPackager -- func packager(ctx *Context, selector ArtifactsSelector) error { - imageName, selectedArtifacts, err := selector(ctx.Artifacts) + err := selector(ctx) if err != nil { return err } - if imageName == "" { - imageName = ctx.Image - } tarFileName := path.Join(ctx.Path, "package", "occi.tar") tarFileDir := path.Dir(tarFileName) @@ -229,7 +240,7 @@ func packager(ctx *Context, selector ArtifactsSelector) error { } defer tarAppender.Close() - for _, entry := range selectedArtifacts { + for _, entry := range ctx.SelectedArtifacts { _, tarFileName := path.Split(entry.Target) tarFilePath := path.Dir(entry.Target) @@ -239,19 +250,23 @@ func packager(ctx *Context, selector ArtifactsSelector) error { } } - if ctx.ComputeClasspath { + for _, entry := range ctx.Request.Resources { + if err := tarAppender.AddData(entry.Content, entry.Target); err != nil { + return err + } + } + + if ctx.ComputeClasspath && len(ctx.Artifacts) > 0 { cp := "" for _, entry := range ctx.Artifacts { - cp += path.Join(entry.Target) + "\n" + cp += entry.Target + "\n" } - err = tarAppender.AppendData([]byte(cp), "classpath") - if err != nil { + if err := tarAppender.AddData([]byte(cp), "classpath"); err != nil { return err } } - ctx.Image = imageName ctx.Archive = tarFileName return nil @@ -275,38 +290,67 @@ func ListPublishedImages(namespace string) ([]PublishedImage, error) { } images = append(images, PublishedImage{ - Image: ctx.Status.Image, - Artifacts: ctx.Status.Artifacts, + Image: ctx.Status.Image, + Artifacts: ctx.Status.Artifacts, + Dependencies: ctx.Spec.Dependencies, }) } return images, nil } // FindBestImage -- -func FindBestImage(images []PublishedImage, entries []v1alpha1.Artifact) (*PublishedImage, map[string]bool) { +func FindBestImage(images []PublishedImage, dependencies []string, artifacts []v1alpha1.Artifact) (PublishedImage, map[string]bool) { + var bestImage PublishedImage + if len(images) == 0 { - return nil, nil + return bestImage, nil } - requiredLibs := make(map[string]bool, len(entries)) - for _, entry := range entries { + + requiredLibs := make(map[string]bool, len(artifacts)) + for _, entry := range artifacts { requiredLibs[entry.ID] = true } - var bestImage PublishedImage + requiredRuntimes := strset.New() + for _, entry := range dependencies { + if strings.HasPrefix(entry, "runtime:") { + requiredRuntimes.Add(entry) + } + } + bestImageCommonLibs := make(map[string]bool) bestImageSurplusLibs := 0 + for _, image := range images { + runtimes := strset.New() + for _, entry := range image.Dependencies { + if strings.HasPrefix(entry, "runtime:") { + runtimes.Add(entry) + } + } + + // + // check if the image has the same runtime requirements to avoid the heuristic + // selector to include unwanted runtime bits such as spring-boot (which may have + // an additional artifact only thus it may match) + // + if !requiredRuntimes.IsSubset(runtimes) { + continue + } + common := make(map[string]bool) for _, artifact := range image.Artifacts { if _, ok := requiredLibs[artifact.ID]; ok { common[artifact.ID] = true } } + numCommonLibs := len(common) surplus := len(image.Artifacts) - numCommonLibs if numCommonLibs != len(image.Artifacts) && surplus >= numCommonLibs/3 { - // Heuristic approach: if there are too many unrelated libraries, just use the base image + // Heuristic approach: if there are too many unrelated libraries, just use + // the base image continue } @@ -317,7 +361,7 @@ func FindBestImage(images []PublishedImage, entries []v1alpha1.Artifact) (*Publi } } - return &bestImage, bestImageCommonLibs + return bestImage, bestImageCommonLibs } // Notify -- diff --git a/pkg/builder/builder_types.go b/pkg/builder/builder_types.go index 1f970f0..47c59e0 100644 --- a/pkg/builder/builder_types.go +++ b/pkg/builder/builder_types.go @@ -94,15 +94,22 @@ func NewStep(ID string, phase int32, task StepTask) Step { return &s } +// Resource -- +type Resource struct { + Target string + Content []byte +} + // Request -- type Request struct { Meta v1.ObjectMeta Platform v1alpha1.IntegrationPlatformSpec - Code v1alpha1.SourceSpec Dependencies []string Repositories []string Steps []Step BuildDir string + Image string + Resources []Resource } // Task -- @@ -128,23 +135,39 @@ type Result struct { // Context -- type Context struct { - C context.Context - Request Request - Image string - Error error - Namespace string - Project maven.Project - Path string - Artifacts []v1alpha1.Artifact - Archive string - ComputeClasspath bool - MainClass string + C context.Context + Request Request + Image string + Error error + Namespace string + Project maven.Project + Path string + Artifacts []v1alpha1.Artifact + SelectedArtifacts []v1alpha1.Artifact + Archive string + ComputeClasspath bool + MainClass string +} + +// HasRequiredImage -- +func (c *Context) HasRequiredImage() bool { + return c.Request.Image != "" +} + +// GetImage -- +func (c *Context) GetImage() string { + if c.Request.Image != "" { + return c.Request.Image + } + + return c.Image } // PublishedImage -- type PublishedImage struct { - Image string - Artifacts []v1alpha1.Artifact + Image string + Artifacts []v1alpha1.Artifact + Dependencies []string } // Status -- diff --git a/pkg/builder/builder_utils.go b/pkg/builder/builder_utils.go index 1a9fb62..8ba81c5 100644 --- a/pkg/builder/builder_utils.go +++ b/pkg/builder/builder_utils.go @@ -17,7 +17,11 @@ limitations under the License. package builder -import "os" +import ( + "os" + + "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" +) // MavenExtraOptions -- func MavenExtraOptions() string { @@ -26,3 +30,14 @@ func MavenExtraOptions() string { } return "-Dcamel.noop=true" } + +// ArtifactIDs -- +func ArtifactIDs(artifacts []v1alpha1.Artifact) []string { + result := make([]string, 0, len(artifacts)) + + for _, a := range artifacts { + result = append(result, a.ID) + } + + return result +} diff --git a/pkg/builder/springboot/initializer.go b/pkg/builder/springboot/initializer.go index de40ea6..2b8b5af 100644 --- a/pkg/builder/springboot/initializer.go +++ b/pkg/builder/springboot/initializer.go @@ -23,9 +23,6 @@ import ( // Initialize -- func Initialize(ctx *builder.Context) error { - // set the base image - //ctx.Image = "kamel-k/s2i-boot:" + version.Version - // no need to compute classpath as we do use spring boot own // loader: PropertiesLauncher ctx.ComputeClasspath = false diff --git a/pkg/stub/action/context/build.go b/pkg/stub/action/context/build.go index 9625545..b3e9226 100644 --- a/pkg/stub/action/context/build.go +++ b/pkg/stub/action/context/build.go @@ -79,10 +79,10 @@ func (action *buildAction) Handle(context *v1alpha1.IntegrationContext) error { target := context.DeepCopy() target.Status.Phase = v1alpha1.IntegrationContextPhaseError - logrus.Info("Context ", target.Name, " transitioning to state ", v1alpha1.IntegrationContextPhaseError) + logrus.Info("Context ", target.Name, " transitioning to state ", target.Status.Phase) // remove the build from cache - b.Purge(r) + defer b.Purge(r) return sdk.Update(target) case builder.StatusCompleted: @@ -100,7 +100,10 @@ func (action *buildAction) Handle(context *v1alpha1.IntegrationContext) error { }) } - logrus.Info("Context ", target.Name, " transitioning to state ", v1alpha1.IntegrationContextPhaseReady) + logrus.Info("Context ", target.Name, " transitioning to state ", target.Status.Phase) + + // remove the build from cache + defer b.Purge(r) if err := sdk.Update(target); err != nil { return err @@ -108,9 +111,6 @@ func (action *buildAction) Handle(context *v1alpha1.IntegrationContext) error { if err := action.informIntegrations(target); err != nil { return err } - - // remove the build from cache - b.Purge(r) } return nil diff --git a/pkg/stub/action/integration/build.go b/pkg/stub/action/integration/build_context.go similarity index 84% rename from pkg/stub/action/integration/build.go rename to pkg/stub/action/integration/build_context.go index 85eb881..679945e 100644 --- a/pkg/stub/action/integration/build.go +++ b/pkg/stub/action/integration/build_context.go @@ -20,6 +20,8 @@ package integration import ( "fmt" + "github.com/apache/camel-k/pkg/trait" + "github.com/sirupsen/logrus" "github.com/apache/camel-k/pkg/util" @@ -31,26 +33,26 @@ import ( "github.com/operator-framework/operator-sdk/pkg/sdk" ) -// NewBuildAction create an action that handles integration build -func NewBuildAction(namespace string) Action { - return &buildAction{ +// NewBuildContextAction create an action that handles integration context build +func NewBuildContextAction(namespace string) Action { + return &buildContextAction{ namespace: namespace, } } -type buildAction struct { +type buildContextAction struct { namespace string } -func (action *buildAction) Name() string { - return "build" +func (action *buildContextAction) Name() string { + return "build-context" } -func (action *buildAction) CanHandle(integration *v1alpha1.Integration) bool { - return integration.Status.Phase == v1alpha1.IntegrationPhaseBuilding +func (action *buildContextAction) CanHandle(integration *v1alpha1.Integration) bool { + return integration.Status.Phase == v1alpha1.IntegrationPhaseBuildingContext } -func (action *buildAction) Handle(integration *v1alpha1.Integration) error { +func (action *buildContextAction) Handle(integration *v1alpha1.Integration) error { ctx, err := LookupContextForIntegration(integration) if err != nil { //TODO: we may need to add a wait strategy, i.e give up after some time @@ -74,30 +76,40 @@ func (action *buildAction) Handle(integration *v1alpha1.Integration) error { } } - if ctx.Status.Phase == v1alpha1.IntegrationContextPhaseReady { + if ctx.Status.Phase == v1alpha1.IntegrationContextPhaseError { target := integration.DeepCopy() target.Status.Image = ctx.Status.Image target.Spec.Context = ctx.Name - logrus.Info("Integration ", target.Name, " transitioning to state ", v1alpha1.IntegrationPhaseDeploying) - target.Status.Phase = v1alpha1.IntegrationPhaseDeploying - dgst, err := digest.ComputeForIntegration(target) + target.Status.Phase = v1alpha1.IntegrationPhaseError + + target.Status.Digest, err = digest.ComputeForIntegration(target) if err != nil { return err } - target.Status.Digest = dgst + + logrus.Info("Integration ", target.Name, " transitioning to state ", target.Status.Phase) + return sdk.Update(target) } - if ctx.Status.Phase == v1alpha1.IntegrationContextPhaseError { + if ctx.Status.Phase == v1alpha1.IntegrationContextPhaseReady { target := integration.DeepCopy() target.Status.Image = ctx.Status.Image target.Spec.Context = ctx.Name - target.Status.Phase = v1alpha1.IntegrationPhaseError + dgst, err := digest.ComputeForIntegration(target) if err != nil { return err } + target.Status.Digest = dgst + + if _, err := trait.Apply(target, ctx); err != nil { + return err + } + + logrus.Info("Integration ", target.Name, " transitioning to state ", target.Status.Phase) + return sdk.Update(target) } @@ -138,5 +150,6 @@ func (action *buildAction) Handle(integration *v1alpha1.Integration) error { // same path as integration with a user defined context target := integration.DeepCopy() target.Spec.Context = platformCtxName + return sdk.Update(target) } diff --git a/pkg/stub/action/integration/build_image.go b/pkg/stub/action/integration/build_image.go new file mode 100644 index 0000000..a30e4bf --- /dev/null +++ b/pkg/stub/action/integration/build_image.go @@ -0,0 +1,144 @@ +/* +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 integration + +import ( + "context" + "fmt" + "path" + + "github.com/pkg/errors" + + "github.com/apache/camel-k/pkg/util/digest" + + "github.com/apache/camel-k/pkg/trait" + + "github.com/apache/camel-k/pkg/builder" + "github.com/operator-framework/operator-sdk/pkg/sdk" + "github.com/sirupsen/logrus" + + "github.com/apache/camel-k/pkg/platform" + + "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" +) + +// NewBuildImageAction create an action that handles integration image build +func NewBuildImageAction(ctx context.Context, namespace string) Action { + return &buildImageAction{ + Context: ctx, + namespace: namespace, + } +} + +type buildImageAction struct { + context.Context + namespace string +} + +func (action *buildImageAction) Name() string { + return "build-image" +} + +func (action *buildImageAction) CanHandle(integration *v1alpha1.Integration) bool { + return integration.Status.Phase == v1alpha1.IntegrationPhaseBuildingImage +} + +func (action *buildImageAction) Handle(integration *v1alpha1.Integration) error { + + // in this phase the integration need to be associated to a context whose image + // will be used as base image for the integration images + if integration.Spec.Context == "" { + return fmt.Errorf("context is not set for integration: %s", integration.Name) + } + + // look-up the integration context associated to this integration, this is needed + // to determine the base image + ctx := v1alpha1.NewIntegrationContext(integration.Namespace, integration.Spec.Context) + if err := sdk.Get(&ctx); err != nil { + return errors.Wrapf(err, "unable to find integration context %s, %s", ctx.Name, err) + } + + b, err := platform.GetPlatformBuilder(action.Context, action.namespace) + if err != nil { + return err + } + env, err := trait.Apply(integration, &ctx) + if err != nil { + return err + } + + // This build do not require to determine dependencies nor a project, the builder + // step do remove them + r := builder.Request{ + Meta: integration.ObjectMeta, + Steps: env.Steps, + Platform: env.Platform.Spec, + Image: ctx.Status.Image, + } + + // Sources are added as part of the standard deployment bits + r.Resources = make([]builder.Resource, 0, len(integration.Spec.Sources)) + + for _, source := range integration.Spec.Sources { + r.Resources = append(r.Resources, builder.Resource{ + Content: []byte(source.Content), + Target: path.Join("sources", source.Name), + }) + } + + res := b.Submit(r) + + switch res.Status { + case builder.StatusSubmitted: + logrus.Info("Build submitted") + case builder.StatusStarted: + logrus.Info("Build started") + case builder.StatusError: + target := integration.DeepCopy() + target.Status.Phase = v1alpha1.IntegrationPhaseError + + logrus.Info("Integration ", target.Name, " transitioning to state ", target.Status.Phase) + + // remove the build from cache + defer b.Purge(r) + + return sdk.Update(target) + case builder.StatusCompleted: + target := integration.DeepCopy() + target.Status.Phase = v1alpha1.IntegrationPhaseDeploying + target.Status.Image = res.Image + + dgst, err := digest.ComputeForIntegration(integration) + if err != nil { + return err + } + + target.Status.Digest = dgst + + logrus.Info("Integration ", target.Name, " transitioning to state ", target.Status.Phase) + + // remove the build from cache + defer b.Purge(r) + + if err := sdk.Update(target); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/stub/action/integration/initialize.go b/pkg/stub/action/integration/initialize.go index 8a21301..eea1957 100644 --- a/pkg/stub/action/integration/initialize.go +++ b/pkg/stub/action/integration/initialize.go @@ -73,12 +73,15 @@ func (action *initializeAction) Handle(integration *v1alpha1.Integration) error } // update the status - logrus.Info("Integration ", target.Name, " transitioning to state ", v1alpha1.IntegrationPhaseBuilding) - target.Status.Phase = v1alpha1.IntegrationPhaseBuilding dgst, err := digest.ComputeForIntegration(integration) if err != nil { return err } + + target.Status.Phase = v1alpha1.IntegrationPhaseBuildingContext target.Status.Digest = dgst + + logrus.Info("Integration ", target.Name, " transitioning to state ", target.Status.Phase) + return sdk.Update(target) } diff --git a/pkg/stub/action/integration/monitor.go b/pkg/stub/action/integration/monitor.go index 756695b..d0e8107 100644 --- a/pkg/stub/action/integration/monitor.go +++ b/pkg/stub/action/integration/monitor.go @@ -53,8 +53,10 @@ func (action *monitorAction) Handle(integration *v1alpha1.Integration) error { target := integration.DeepCopy() target.Status.Digest = hash - logrus.Info("Integration ", target.Name, " transitioning to state ", v1alpha1.IntegrationPhaseBuilding) - target.Status.Phase = v1alpha1.IntegrationPhaseBuilding + target.Status.Phase = v1alpha1.IntegrationPhaseBuildingContext + + logrus.Info("Integration ", target.Name, " transitioning to state ", target.Status.Phase) + return sdk.Update(target) } diff --git a/pkg/stub/handler.go b/pkg/stub/handler.go index a2f98ca..f4d3eaa 100644 --- a/pkg/stub/handler.go +++ b/pkg/stub/handler.go @@ -34,7 +34,8 @@ func NewHandler(ctx ctx.Context, namespace string) sdk.Handler { return &handler{ integrationActionPool: []integration.Action{ integration.NewInitializeAction(), - integration.NewBuildAction(namespace), + integration.NewBuildContextAction(namespace), + integration.NewBuildImageAction(ctx, namespace), integration.NewDeployAction(), integration.NewMonitorAction(), }, diff --git a/pkg/trait/builder.go b/pkg/trait/builder.go index c8c4a50..84702cb 100644 --- a/pkg/trait/builder.go +++ b/pkg/trait/builder.go @@ -19,6 +19,7 @@ package trait import ( "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" + "github.com/apache/camel-k/pkg/builder" "github.com/apache/camel-k/pkg/builder/kaniko" "github.com/apache/camel-k/pkg/builder/s2i" "github.com/apache/camel-k/pkg/platform" @@ -36,14 +37,41 @@ func newBuilderTrait() *builderTrait { } func (*builderTrait) appliesTo(e *Environment) bool { - return e.Context != nil && e.Context.Status.Phase == v1alpha1.IntegrationContextPhaseBuilding + if e.Context != nil && e.Context.Status.Phase == v1alpha1.IntegrationContextPhaseBuilding { + return true + } + + if e.Integration != nil && e.Integration.Status.Phase == v1alpha1.IntegrationPhaseBuildingImage && + e.Context != nil && e.Context.Status.Phase == v1alpha1.IntegrationContextPhaseReady { + return true + } + + return false } func (*builderTrait) apply(e *Environment) error { - if platform.SupportsS2iPublishStrategy(e.Platform) { - e.Steps = s2i.DefaultSteps - } else if platform.SupportsKanikoPublishStrategy(e.Platform) { - e.Steps = kaniko.DefaultSteps + if e.Context != nil && e.Context.Status.Phase == v1alpha1.IntegrationContextPhaseBuilding { + if platform.SupportsS2iPublishStrategy(e.Platform) { + e.Steps = s2i.DefaultSteps + } else if platform.SupportsKanikoPublishStrategy(e.Platform) { + e.Steps = kaniko.DefaultSteps + } + } + + if e.Integration != nil && e.Integration.Status.Phase == v1alpha1.IntegrationPhaseBuildingImage && + e.Context != nil && e.Context.Status.Phase == v1alpha1.IntegrationContextPhaseReady { + + if platform.SupportsS2iPublishStrategy(e.Platform) { + e.Steps = []builder.Step{ + builder.NewStep("packager", builder.ApplicationPackagePhase, builder.StandardPackager), + builder.NewStep("publisher/s2i", builder.ApplicationPublishPhase, s2i.Publisher), + } + } else if platform.SupportsKanikoPublishStrategy(e.Platform) { + e.Steps = []builder.Step{ + builder.NewStep("packager", builder.ApplicationPackagePhase, builder.StandardPackager), + builder.NewStep("publisher/kaniko", builder.ApplicationPublishPhase, kaniko.Publisher), + } + } } return nil diff --git a/pkg/trait/catalog.go b/pkg/trait/catalog.go index 62c2f9f..d0180be 100644 --- a/pkg/trait/catalog.go +++ b/pkg/trait/catalog.go @@ -73,9 +73,7 @@ func (c *Catalog) allTraits() []Trait { } func (c *Catalog) traitsFor(environment *Environment) []Trait { - profile := environment.DetermineProfile() - - switch profile { + switch environment.DetermineProfile() { case v1alpha1.TraitProfileOpenShift: return []Trait{ c.tDebug, @@ -105,6 +103,7 @@ func (c *Catalog) traitsFor(environment *Environment) []Trait { c.tKnative, c.tBuilder, c.tSpringBoot, + c.tDeployment, c.tOwner, } } @@ -128,14 +127,17 @@ func (c *Catalog) apply(environment *Environment) error { return err } } + if trait.IsEnabled() { logrus.Infof("apply trait: %s", trait.ID()) if err := trait.apply(environment); err != nil { return err } + environment.ExecutedTraits = append(environment.ExecutedTraits, trait.ID()) } } + return nil } diff --git a/pkg/trait/deployment.go b/pkg/trait/deployment.go index 7304aa2..b3a3be6 100644 --- a/pkg/trait/deployment.go +++ b/pkg/trait/deployment.go @@ -19,6 +19,7 @@ package trait import ( "fmt" + "path" "strings" "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" @@ -29,7 +30,8 @@ import ( ) type deploymentTrait struct { - BaseTrait `property:",squash"` + BaseTrait `property:",squash"` + ContainerImage bool `property:"container-image"` } func newDeploymentTrait() *deploymentTrait { @@ -39,12 +41,40 @@ func newDeploymentTrait() *deploymentTrait { } func (d *deploymentTrait) appliesTo(e *Environment) bool { - return e.Integration != nil && e.Integration.Status.Phase == v1alpha1.IntegrationPhaseDeploying + if e.Integration != nil && e.Integration.Status.Phase == v1alpha1.IntegrationPhaseDeploying { + // + // Don't deploy on knative + // + return e.DetermineProfile() != v1alpha1.TraitProfileKnative + } + + if d.ContainerImage && e.InPhase(v1alpha1.IntegrationContextPhaseReady, v1alpha1.IntegrationPhaseBuildingContext) { + return true + } + + if !d.ContainerImage && e.InPhase(v1alpha1.IntegrationContextPhaseReady, v1alpha1.IntegrationPhaseBuildingContext) { + return true + } + + return false } func (d *deploymentTrait) apply(e *Environment) error { - e.Resources.AddAll(getConfigMapsFor(e)) - e.Resources.Add(getDeploymentFor(e)) + if d.ContainerImage && e.InPhase(v1alpha1.IntegrationContextPhaseReady, v1alpha1.IntegrationPhaseBuildingContext) { + // trigger container image build + e.Integration.Status.Phase = v1alpha1.IntegrationPhaseBuildingImage + } + + if !d.ContainerImage && e.InPhase(v1alpha1.IntegrationContextPhaseReady, v1alpha1.IntegrationPhaseBuildingContext) { + // trigger integration deploy + e.Integration.Status.Phase = v1alpha1.IntegrationPhaseDeploying + } + + if e.Integration != nil && e.Integration.Status.Phase == v1alpha1.IntegrationPhaseDeploying { + e.Resources.AddAll(d.getConfigMapsFor(e)) + e.Resources.Add(d.getDeploymentFor(e)) + } + return nil } @@ -54,7 +84,7 @@ func (d *deploymentTrait) apply(e *Environment) error { // // ********************************** -func getConfigMapsFor(e *Environment) []runtime.Object { +func (d *deploymentTrait) getConfigMapsFor(e *Environment) []runtime.Object { maps := make([]runtime.Object, 0, len(e.Integration.Spec.Sources)+1) // combine properties of integration with context, integration @@ -81,30 +111,35 @@ func getConfigMapsFor(e *Environment) []runtime.Object { }, ) - for i, s := range e.Integration.Spec.Sources { - maps = append( - maps, - &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-source-%03d", e.Integration.Name, i), - Namespace: e.Integration.Namespace, - Labels: map[string]string{ - "camel.apache.org/integration": e.Integration.Name, + if !d.ContainerImage { + + // do not create 'source' ConfigMap if a docker images for deployment + // is required + for i, s := range e.Integration.Spec.Sources { + maps = append( + maps, + &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", }, - Annotations: map[string]string{ - "camel.apache.org/source.language": string(s.Language), - "camel.apache.org/source.name": s.Name, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-source-%03d", e.Integration.Name, i), + Namespace: e.Integration.Namespace, + Labels: map[string]string{ + "camel.apache.org/integration": e.Integration.Name, + }, + Annotations: map[string]string{ + "camel.apache.org/source.language": string(s.Language), + "camel.apache.org/source.name": s.Name, + }, + }, + Data: map[string]string{ + "integration": s.Content, }, }, - Data: map[string]string{ - "integration": s.Content, - }, - }, - ) + ) + } } return maps @@ -116,17 +151,34 @@ func getConfigMapsFor(e *Environment) []runtime.Object { // // ********************************** -func getDeploymentFor(e *Environment) *appsv1.Deployment { +func (d *deploymentTrait) getSources(e *Environment) []string { sources := make([]string, 0, len(e.Integration.Spec.Sources)) + for i, s := range e.Integration.Spec.Sources { - src := fmt.Sprintf("file:/etc/camel/integrations/%03d/%s", i, strings.TrimPrefix(s.Name, "/")) + root := fmt.Sprintf("/etc/camel/integrations/%03d", i) + + if d.ContainerImage { + + // assume sources are copied over the standard deployments folder + root = "/deployments/sources" + } + + src := path.Join(root, s.Name) + src = "file:" + src + if s.Language != "" { - src = src + "?language=" + string(s.Language) + src = fmt.Sprintf("%s?language=%s", src, string(s.Language)) } sources = append(sources, src) } + return sources +} + +func (d *deploymentTrait) getDeploymentFor(e *Environment) *appsv1.Deployment { + sources := d.getSources(e) + // combine Environment of integration with context, integration // Environment has the priority environment := CombineConfigurationAsMap("env", e.Context, e.Integration) @@ -227,28 +279,35 @@ func getDeploymentFor(e *Environment) *appsv1.Deployment { // Volumes :: Sources // - for i, s := range e.Integration.Spec.Sources { - vols = append(vols, corev1.Volume{ - Name: fmt.Sprintf("integration-source-%03d", i), - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: fmt.Sprintf("%s-source-%03d", e.Integration.Name, i), - }, - Items: []corev1.KeyToPath{ - { - Key: "integration", - Path: strings.TrimPrefix(s.Name, "/"), + if !d.ContainerImage { + + // We can configure the operator to generate a container images that include + // integration sources instead of mounting it at runtime and in such case we + // do not need to mount any 'source' ConfigMap to the pod + + for i, s := range e.Integration.Spec.Sources { + vols = append(vols, corev1.Volume{ + Name: fmt.Sprintf("integration-source-%03d", i), + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: fmt.Sprintf("%s-source-%03d", e.Integration.Name, i), + }, + Items: []corev1.KeyToPath{ + { + Key: "integration", + Path: strings.TrimPrefix(s.Name, "/"), + }, }, }, }, - }, - }) + }) - mnts = append(mnts, corev1.VolumeMount{ - Name: fmt.Sprintf("integration-source-%03d", i), - MountPath: fmt.Sprintf("/etc/camel/integrations/%03d", i), - }) + mnts = append(mnts, corev1.VolumeMount{ + Name: fmt.Sprintf("integration-source-%03d", i), + MountPath: fmt.Sprintf("/etc/camel/integrations/%03d", i), + }) + } } // diff --git a/pkg/trait/types.go b/pkg/trait/types.go index eaaba02..53fb69c 100644 --- a/pkg/trait/types.go +++ b/pkg/trait/types.go @@ -110,7 +110,12 @@ func (e *Environment) IntegrationContextInPhase(phase v1alpha1.IntegrationContex return e.Context != nil && e.Context.Status.Phase == phase } -// DeterimeProfile determines the TraitProfile of the environment. +// InPhase -- +func (e *Environment) InPhase(c v1alpha1.IntegrationContextPhase, i v1alpha1.IntegrationPhase) bool { + return e.IntegrationContextInPhase(c) && e.IntegrationInPhase(i) +} + +// DetermineProfile determines the TraitProfile of the environment. // First looking at the Integration.Spec for a Profile, // next looking at the Context.Spec // and lastly the Platform Profile diff --git a/pkg/util/tar/appender.go b/pkg/util/tar/appender.go index 78df669..47bb8a4 100644 --- a/pkg/util/tar/appender.go +++ b/pkg/util/tar/appender.go @@ -128,19 +128,21 @@ func (t *Appender) AddFileWithName(fileName string, filePath string, tarDir stri return fileName, nil } -// AppendData appends the given content to a file inside the tar, creating it if it does not exist -func (t *Appender) AppendData(data []byte, tarPath string) error { - if err := t.writer.WriteHeader(&atar.Header{ +// AddData appends the given content to a file inside the tar, creating it if it does not exist +func (t *Appender) AddData(data []byte, tarPath string) error { + err := t.writer.WriteHeader(&atar.Header{ Name: tarPath, Size: int64(len(data)), Mode: 0644, - }); err != nil { + }) + + if err != nil { return err } - _, err := t.writer.Write(data) - if err != nil { + if _, err := t.writer.Write(data); err != nil { return errors.Wrap(err, "cannot add data to the tar archive") } + return nil } diff --git a/test/build_manager_integration_test.go b/test/build_manager_integration_test.go index 1e8a10c..4bbcd19 100644 --- a/test/build_manager_integration_test.go +++ b/test/build_manager_integration_test.go @@ -28,7 +28,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" "github.com/apache/camel-k/pkg/builder" "github.com/apache/camel-k/pkg/builder/s2i" "github.com/stretchr/testify/assert" @@ -44,9 +43,6 @@ func TestBuildManagerBuild(t *testing.T) { Name: "man-test", ResourceVersion: "1", }, - Code: v1alpha1.SourceSpec{ - Content: createTimerToLogIntegrationCode(), - }, Dependencies: []string{ "mvn:org.apache.camel/camel-core", "camel:telegram", @@ -83,9 +79,6 @@ func TestBuildManagerFailedBuild(t *testing.T) { Name: "man-test", ResourceVersion: "1", }, - Code: v1alpha1.SourceSpec{ - Content: createTimerToLogIntegrationCode(), - }, Dependencies: []string{ "mvn:org.apache.camel/camel-cippalippa", }, diff --git a/test/testing_env.go b/test/testing_env.go index 66ff683..d8265c4 100644 --- a/test/testing_env.go +++ b/test/testing_env.go @@ -56,22 +56,6 @@ func getTargetNamespace() string { return ns } -func createTimerToLogIntegrationCode() string { - return ` -import org.apache.camel.builder.RouteBuilder; - -public class Routes extends RouteBuilder { - - @Override - public void configure() throws Exception { - from("timer:tick") - .to("log:info"); - } - -} -` -} - func createDummyDeployment(name string, replicas *int32, labelKey string, labelValue string, command ...string) (*appsv1.Deployment, error) { deployment := getDummyDeployment(name, replicas, labelKey, labelValue, command...) gracePeriod := int64(0) diff --git a/vendor/github.com/scylladb/go-set/LICENSE b/vendor/github.com/scylladb/go-set/LICENSE new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/vendor/github.com/scylladb/go-set/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/vendor/github.com/scylladb/go-set/strset/strset.go b/vendor/github.com/scylladb/go-set/strset/strset.go new file mode 100644 index 0000000..ed828e5 --- /dev/null +++ b/vendor/github.com/scylladb/go-set/strset/strset.go @@ -0,0 +1,279 @@ +// Copyright (C) 2017 ScyllaDB +// Use of this source code is governed by a ALv2-style +// license that can be found at https://github.com/scylladb/go-set/LICENSE. + +package strset + +import ( + "fmt" + "math" + "strings" +) + +var ( + // helpful to not write everywhere struct{}{} + keyExists = struct{}{} + nonExistent string +) + +// Set is the main set structure that holds all the data +// and methods used to working with the set. +type Set struct { + m map[string]struct{} +} + +// New creates and initializes a new Set. +func New(ts ...string) *Set { + s := NewWithSize(len(ts)) + s.Add(ts...) + return s +} + +// NewWithSize creates a new Set and gives make map a size hint. +func NewWithSize(size int) *Set { + return &Set{make(map[string]struct{}, size)} +} + +// Add includes the specified items (one or more) to the Set. The underlying +// Set s is modified. If passed nothing it silently returns. +func (s *Set) Add(items ...string) { + for _, item := range items { + s.m[item] = keyExists + } +} + +// Remove deletes the specified items from the Set. The underlying Set s is +// modified. If passed nothing it silently returns. +func (s *Set) Remove(items ...string) { + for _, item := range items { + delete(s.m, item) + } +} + +// Pop deletes and returns an item from the Set. The underlying Set s is +// modified. If Set is empty, the zero value is returned. +func (s *Set) Pop() string { + for item := range s.m { + delete(s.m, item) + return item + } + return nonExistent +} + +// Pop2 tries to delete and return an item from the Set. The underlying Set s +// is modified. The second value is a bool that is true if the item existed in +// the set, and false if not. If Set is empty, the zero value and false are +// returned. +func (s *Set) Pop2() (string, bool) { + for item := range s.m { + delete(s.m, item) + return item, true + } + return nonExistent, false +} + +// Has looks for the existence of items passed. It returns false if nothing is +// passed. For multiple items it returns true only if all of the items exist. +func (s *Set) Has(items ...string) bool { + has := false + for _, item := range items { + if _, has = s.m[item]; !has { + break + } + } + return has +} + +// HasAny looks for the existence of any of the items passed. +// It returns false if nothing is passed. +// For multiple items it returns true if any of the items exist. +func (s *Set) HasAny(items ...string) bool { + has := false + for _, item := range items { + if _, has = s.m[item]; has { + break + } + } + return has +} + +// Size returns the number of items in a Set. +func (s *Set) Size() int { + return len(s.m) +} + +// Clear removes all items from the Set. +func (s *Set) Clear() { + s.m = make(map[string]struct{}) +} + +// IsEmpty reports whether the Set is empty. +func (s *Set) IsEmpty() bool { + return s.Size() == 0 +} + +// IsEqual test whether s and t are the same in size and have the same items. +func (s *Set) IsEqual(t *Set) bool { + // return false if they are no the same size + if s.Size() != t.Size() { + return false + } + + equal := true + t.Each(func(item string) bool { + _, equal = s.m[item] + return equal // if false, Each() will end + }) + + return equal +} + +// IsSubset tests whether t is a subset of s. +func (s *Set) IsSubset(t *Set) bool { + if s.Size() < t.Size() { + return false + } + + subset := true + + t.Each(func(item string) bool { + _, subset = s.m[item] + return subset + }) + + return subset +} + +// IsSuperset tests whether t is a superset of s. +func (s *Set) IsSuperset(t *Set) bool { + return t.IsSubset(s) +} + +// Each traverses the items in the Set, calling the provided function for each +// Set member. Traversal will continue until all items in the Set have been +// visited, or if the closure returns false. +func (s *Set) Each(f func(item string) bool) { + for item := range s.m { + if !f(item) { + break + } + } +} + +// Copy returns a new Set with a copy of s. +func (s *Set) Copy() *Set { + u := NewWithSize(s.Size()) + for item := range s.m { + u.m[item] = keyExists + } + return u +} + +// String returns a string representation of s +func (s *Set) String() string { + v := make([]string, 0, s.Size()) + for item := range s.m { + v = append(v, fmt.Sprintf("%v", item)) + } + return fmt.Sprintf("[%s]", strings.Join(v, ", ")) +} + +// List returns a slice of all items. There is also StringSlice() and +// IntSlice() methods for returning slices of type string or int. +func (s *Set) List() []string { + v := make([]string, 0, s.Size()) + for item := range s.m { + v = append(v, item) + } + return v +} + +// Merge is like Union, however it modifies the current Set it's applied on +// with the given t Set. +func (s *Set) Merge(t *Set) { + for item := range t.m { + s.m[item] = keyExists + } +} + +// Separate removes the Set items containing in t from Set s. Please aware that +// it's not the opposite of Merge. +func (s *Set) Separate(t *Set) { + for item := range t.m { + delete(s.m, item) + } +} + +// Union is the merger of multiple sets. It returns a new set with all the +// elements present in all the sets that are passed. +func Union(sets ...*Set) *Set { + maxPos := -1 + maxSize := 0 + for i, set := range sets { + if l := set.Size(); l > maxSize { + maxSize = l + maxPos = i + } + } + if maxSize == 0 { + return New() + } + + u := sets[maxPos].Copy() + for i, set := range sets { + if i == maxPos { + continue + } + for item := range set.m { + u.m[item] = keyExists + } + } + return u +} + +// Difference returns a new set which contains items which are in in the first +// set but not in the others. +func Difference(set1 *Set, sets ...*Set) *Set { + s := set1.Copy() + for _, set := range sets { + s.Separate(set) + } + return s +} + +// Intersection returns a new set which contains items that only exist in all +// given sets. +func Intersection(sets ...*Set) *Set { + minPos := -1 + minSize := math.MaxInt64 + for i, set := range sets { + if l := set.Size(); l < minSize { + minSize = l + minPos = i + } + } + if minSize == math.MaxInt64 || minSize == 0 { + return New() + } + + t := sets[minPos].Copy() + for i, set := range sets { + if i == minPos { + continue + } + for item := range t.m { + if _, has := set.m[item]; !has { + delete(t.m, item) + } + } + } + return t +} + +// SymmetricDifference returns a new set which s is the difference of items +// which are in one of either, but not in both. +func SymmetricDifference(s *Set, t *Set) *Set { + u := Difference(s, t) + v := Difference(t, s) + return Union(u, v) +}