This is an automated email from the ASF dual-hosted git repository. tsato pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-k.git
The following commit(s) were added to refs/heads/main by this push: new 37a9fadb9 feat(cli): Add add-repo command to add a repo for custom Kamelet catalog 37a9fadb9 is described below commit 37a9fadb90003aa2344d6d700d90bf6a1bef3849 Author: Nicolas Filotto <nfilo...@talend.com> AuthorDate: Thu Sep 8 19:18:31 2022 +0200 feat(cli): Add add-repo command to add a repo for custom Kamelet catalog --- .../files/TimerCustomKameletIntegration.java | 28 ++++++ .../kamelets/timer-custom-source.kamelet.yaml | 69 +++++++++++++ e2e/global/common/kamelet_test.go | 43 +++++--- pkg/cmd/kamelet.go | 1 + pkg/cmd/kamelet_add_repo.go | 111 +++++++++++++++++++++ pkg/cmd/kamelet_add_repo_test.go | 90 +++++++++++++++++ pkg/cmd/kamelet_get.go | 5 +- pkg/cmd/kit_get.go | 5 +- 8 files changed, 332 insertions(+), 20 deletions(-) diff --git a/e2e/global/common/files/TimerCustomKameletIntegration.java b/e2e/global/common/files/TimerCustomKameletIntegration.java new file mode 100644 index 000000000..9f749d834 --- /dev/null +++ b/e2e/global/common/files/TimerCustomKameletIntegration.java @@ -0,0 +1,28 @@ +/* + * 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. + */ + +import java.lang.Exception; +import java.lang.Override; +import org.apache.camel.builder.RouteBuilder; + +public class TimerCustomKameletIntegration extends RouteBuilder { + @Override + public void configure() throws Exception { + from("kamelet:timer-custom-source?message=great%20message") + .to("log:info"); + } +} diff --git a/e2e/global/common/files/kamelets/timer-custom-source.kamelet.yaml b/e2e/global/common/files/kamelets/timer-custom-source.kamelet.yaml new file mode 100644 index 000000000..a47e62e0d --- /dev/null +++ b/e2e/global/common/files/kamelets/timer-custom-source.kamelet.yaml @@ -0,0 +1,69 @@ +# --------------------------------------------------------------------------- +# 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. +# --------------------------------------------------------------------------- + +apiVersion: camel.apache.org/v1alpha1 +kind: Kamelet +metadata: + name: timer-custom-source + annotations: + camel.apache.org/kamelet.support.level: "Preview" + camel.apache.org/catalog.version: "main-SNAPSHOT" + camel.apache.org/kamelet.icon: data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gU3ZnIFZlY3RvciBJY29ucyA6IGh0dHA6Ly93d3cub25saW5ld2ViZm9udHMuY29tL2ljb24gLS0+DQo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm9 [...] + camel.apache.org/provider: "Apache Software Foundation" + camel.apache.org/kamelet.group: "Timer" + labels: + camel.apache.org/kamelet.type: source + camel.apache.org/kamelet.verified: "true" +spec: + definition: + title: Timer Custom Source + description: Produces periodic events with a custom payload in a custom way. + required: + - message + type: object + properties: + period: + title: Period + description: The interval between two events in milliseconds + type: integer + default: 1000 + message: + title: Message + description: The message to generate + type: string + example: hello world + contentType: + title: Content Type + description: The content type of the message being generated + type: string + default: text/plain + dependencies: + - "camel:core" + - "camel:timer" + - "camel:kamelet" + template: + from: + uri: timer:tick + parameters: + period: "{{period}}" + steps: + - set-body: + constant: "{{message}}" + - set-header: + name: "Content-Type" + constant: "{{contentType}}" + - to: kamelet:sink diff --git a/e2e/global/common/kamelet_test.go b/e2e/global/common/kamelet_test.go index d255f7319..de91fbe80 100644 --- a/e2e/global/common/kamelet_test.go +++ b/e2e/global/common/kamelet_test.go @@ -41,18 +41,37 @@ func TestKameletClasspathLoading(t *testing.T) { Eventually(Kamelet(kameletName, ns)).Should(BeNil()) - Expect(KamelRunWithID(operatorID, ns, "files/TimerKameletIntegration.java", "-t", "kamelets.enabled=false", - "--resource", "file:files/timer-source.kamelet.yaml@/kamelets/timer-source.kamelet.yaml", - "-p camel.component.kamelet.location=file:/kamelets", - "-d", "camel:yaml-dsl", - // kamelet dependencies - "-d", "camel:timer").Execute()).To(Succeed()) - Eventually(IntegrationPodPhase(ns, "timer-kamelet-integration"), TestTimeoutLong).Should(Equal(corev1.PodRunning)) - - Eventually(IntegrationLogs(ns, "timer-kamelet-integration")).Should(ContainSubstring("important message")) - - // Cleanup - Expect(Kamel("delete", "--all", "-n", ns).Execute()).Should(BeNil()) + // Basic + t.Run("test basic case", func(t *testing.T) { + + Expect(KamelRunWithID(operatorID, ns, "files/TimerKameletIntegration.java", "-t", "kamelets.enabled=false", + "--resource", "file:files/timer-source.kamelet.yaml@/kamelets/timer-source.kamelet.yaml", + "-p camel.component.kamelet.location=file:/kamelets", + "-d", "camel:yaml-dsl", + // kamelet dependencies + "-d", "camel:timer").Execute()).To(Succeed()) + Eventually(IntegrationPodPhase(ns, "timer-kamelet-integration"), TestTimeoutLong).Should(Equal(corev1.PodRunning)) + + Eventually(IntegrationLogs(ns, "timer-kamelet-integration")).Should(ContainSubstring("important message")) + + // Cleanup + Expect(Kamel("delete", "--all", "-n", ns).Execute()).Should(BeNil()) + }) + + // Custom repo + t.Run("test custom Kamelet repository", func(t *testing.T) { + + // Add the custom repository + Expect(Kamel("kamelet", "add-repo", "github:essobedo/camel-k-test/kamelets", "-n", ns, "-x", operatorID).Execute()).To(Succeed()) + + Expect(KamelRunWithID(operatorID, ns, "files/TimerCustomKameletIntegration.java").Execute()).To(Succeed()) + Eventually(IntegrationPodPhase(ns, "timer-custom-kamelet-integration"), TestTimeoutLong).Should(Equal(corev1.PodRunning)) + + Eventually(IntegrationLogs(ns, "timer-custom-kamelet-integration")).Should(ContainSubstring("great message")) + + // Cleanup + Expect(Kamel("delete", "--all", "-n", ns).Execute()).Should(BeNil()) + }) }) } diff --git a/pkg/cmd/kamelet.go b/pkg/cmd/kamelet.go index 89a09062c..7645fd0c2 100644 --- a/pkg/cmd/kamelet.go +++ b/pkg/cmd/kamelet.go @@ -30,6 +30,7 @@ func newCmdKamelet(rootCmdOptions *RootCmdOptions) *cobra.Command { cmd.AddCommand(cmdOnly(newKameletGetCmd(rootCmdOptions))) cmd.AddCommand(cmdOnly(newKameletDeleteCmd(rootCmdOptions))) + cmd.AddCommand(cmdOnly(newKameletAddRepoCmd(rootCmdOptions))) return &cmd } diff --git a/pkg/cmd/kamelet_add_repo.go b/pkg/cmd/kamelet_add_repo.go new file mode 100644 index 000000000..92591823d --- /dev/null +++ b/pkg/cmd/kamelet_add_repo.go @@ -0,0 +1,111 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "errors" + "fmt" + "regexp" + + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" + "github.com/spf13/cobra" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// kameletRepositoryURIRegexp is the regular expression used to validate the URI of a Kamelet repository. +var kameletRepositoryURIRegexp = regexp.MustCompile(`^github:[^/]+/[^/]+((/[^/]+)*)?$`) + +func newKameletAddRepoCmd(rootCmdOptions *RootCmdOptions) (*cobra.Command, *kameletAddRepoCommandOptions) { + options := kameletAddRepoCommandOptions{ + RootCmdOptions: rootCmdOptions, + } + + cmd := cobra.Command{ + Use: "add-repo github:owner/repo[/path_to_kamelets_folder][@version] ...", + Short: "Add a Kamelet repository", + Long: `Add a Kamelet repository.`, + PreRunE: decode(&options), + RunE: func(cmd *cobra.Command, args []string) error { + if err := options.validate(args); err != nil { + return err + } + return options.run(cmd, args) + }, + } + + cmd.Flags().StringP("operator-id", "x", "camel-k", "Id of the Operator to update.") + + return &cmd, &options +} + +type kameletAddRepoCommandOptions struct { + *RootCmdOptions + OperatorID string `mapstructure:"operator-id" yaml:",omitempty"` +} + +func (o *kameletAddRepoCommandOptions) validate(args []string) error { + if len(args) == 0 { + return errors.New("at least one Kamelet repository is expected") + } + if o.OperatorID == "" { + return fmt.Errorf("cannot use empty operator id") + } + return nil +} + +func (o *kameletAddRepoCommandOptions) run(cmd *cobra.Command, args []string) error { + c, err := o.GetCmdClient() + if err != nil { + return err + } + key := client.ObjectKey{ + Namespace: o.Namespace, + Name: o.OperatorID, + } + platform := v1.IntegrationPlatform{} + if err := c.Get(o.Context, key, &platform); err != nil { + if k8serrors.IsNotFound(err) { + // IntegrationPlatform may be in the operator namespace, but we currently don't have a way to determine it: we just warn + fmt.Fprintf(cmd.ErrOrStderr(), "Warning: IntegrationPlatform %q not found in namespace %q\n", key.Name, key.Namespace) + return nil + } + return err + } + for _, uri := range args { + if err := checkURI(uri, platform.Spec.Kamelet.Repositories); err != nil { + return err + } + platform.Spec.Kamelet.Repositories = append(platform.Spec.Kamelet.Repositories, v1.IntegrationPlatformKameletRepositorySpec{ + URI: uri, + }) + } + return c.Update(o.Context, &platform) +} + +func checkURI(uri string, repositories []v1.IntegrationPlatformKameletRepositorySpec) error { + if !kameletRepositoryURIRegexp.MatchString(uri) { + return fmt.Errorf("malformed Kamelet repository uri %s, the expected format is github:owner/repo[/path_to_kamelets_folder][@version]", uri) + } + for _, repo := range repositories { + if repo.URI == uri { + return fmt.Errorf("duplicate Kamelet repository uri %s", uri) + } + } + return nil +} diff --git a/pkg/cmd/kamelet_add_repo_test.go b/pkg/cmd/kamelet_add_repo_test.go new file mode 100644 index 000000000..fcaeeac6c --- /dev/null +++ b/pkg/cmd/kamelet_add_repo_test.go @@ -0,0 +1,90 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" + "github.com/apache/camel-k/pkg/util/test" +) + +const cmdKameletAddRepo = "add-repo" + +// nolint: unparam +func initializeKameletAddRepoCmdOptions(t *testing.T) (*kameletAddRepoCommandOptions, *cobra.Command, RootCmdOptions) { + t.Helper() + + options, rootCmd := kamelTestPreAddCommandInit() + kameletAddRepoCommandOptions := addTestKameletAddRepoCmd(*options, rootCmd) + kamelTestPostAddCommandInit(t, rootCmd) + + return kameletAddRepoCommandOptions, rootCmd, *options +} + +func addTestKameletAddRepoCmd(options RootCmdOptions, rootCmd *cobra.Command) *kameletAddRepoCommandOptions { + // Add a testing version of kamelet add-repo Command + kameletAddRepoCmd, kameletAddRepoOptions := newKameletAddRepoCmd(&options) + kameletAddRepoCmd.RunE = func(c *cobra.Command, args []string) error { + return nil + } + kameletAddRepoCmd.PostRunE = func(c *cobra.Command, args []string) error { + return nil + } + kameletAddRepoCmd.Args = test.ArbitraryArgs + rootCmd.AddCommand(kameletAddRepoCmd) + return kameletAddRepoOptions +} + +func TestKameletAddRepoNoFlag(t *testing.T) { + _, rootCmd, _ := initializeKameletAddRepoCmdOptions(t) + _, err := test.ExecuteCommand(rootCmd, cmdKameletAddRepo, "foo") + assert.Nil(t, err) +} + +func TestKameletAddRepoNonExistingFlag(t *testing.T) { + _, rootCmd, _ := initializeKameletAddRepoCmdOptions(t) + _, err := test.ExecuteCommand(rootCmd, cmdKameletAddRepo, "--nonExistingFlag", "foo") + assert.NotNil(t, err) +} + +func TestKameletAddRepoInvalidRepositoryURI(t *testing.T) { + repositories := []v1.IntegrationPlatformKameletRepositorySpec{} + assert.NotNil(t, checkURI("foo", repositories)) + assert.NotNil(t, checkURI("github", repositories)) + assert.NotNil(t, checkURI("github:", repositories)) + assert.NotNil(t, checkURI("github:foo", repositories)) + assert.NotNil(t, checkURI("github:foo/", repositories)) +} + +func TestKameletAddRepoValidRepositoryURI(t *testing.T) { + repositories := []v1.IntegrationPlatformKameletRepositorySpec{} + assert.Nil(t, checkURI("github:foo/bar", repositories)) + assert.Nil(t, checkURI("github:foo/bar/some/path", repositories)) + assert.Nil(t, checkURI("github:foo/bar@1.0", repositories)) + assert.Nil(t, checkURI("github:foo/bar/some/path@1.0", repositories)) +} + +func TestKameletAddRepoDuplicateRepositoryURI(t *testing.T) { + repositories := []v1.IntegrationPlatformKameletRepositorySpec{{URI: "github:foo/bar"}} + assert.NotNil(t, checkURI("github:foo/bar", repositories)) + assert.Nil(t, checkURI("github:foo/bar2", repositories)) +} diff --git a/pkg/cmd/kamelet_get.go b/pkg/cmd/kamelet_get.go index 013e92c0f..ced23216a 100644 --- a/pkg/cmd/kamelet_get.go +++ b/pkg/cmd/kamelet_get.go @@ -44,11 +44,8 @@ func newKameletGetCmd(rootCmdOptions *RootCmdOptions) (*cobra.Command, *kameletG if err := options.validate(); err != nil { return err } - if err := options.run(cmd); err != nil { - fmt.Fprintln(cmd.ErrOrStderr(), err.Error()) - } - return nil + return options.run(cmd) }, } diff --git a/pkg/cmd/kit_get.go b/pkg/cmd/kit_get.go index e74864897..47f78268a 100644 --- a/pkg/cmd/kit_get.go +++ b/pkg/cmd/kit_get.go @@ -42,11 +42,8 @@ func newKitGetCmd(rootCmdOptions *RootCmdOptions) (*cobra.Command, *kitGetComman if err := options.validate(cmd, args); err != nil { return err } - if err := options.run(cmd); err != nil { - fmt.Fprintln(cmd.ErrOrStderr(), err.Error()) - } - return nil + return options.run(cmd) }, }