Repository: apache/camel-k
Pull request #4414 (2023-05-25 07:39:20)

[Backport-1.12.x] chore: Improve Camel dependency validation
- Reuse logic in validate function just logging a warning and those validate function raising errors
- Remove unused function ValidateDependencies
- Add some unit tests
- Add some documentation on Camel dependency resolution via catalog

Manual backport of #4411 


Patch:
diff --git a/docs/modules/ROOT/pages/configuration/dependencies.adoc b/docs/modules/ROOT/pages/configuration/dependencies.adoc
index 40062af3b..d0cdeddd7 100644
--- a/docs/modules/ROOT/pages/configuration/dependencies.adoc
+++ b/docs/modules/ROOT/pages/configuration/dependencies.adoc
@@ -39,6 +39,13 @@ kamel run -d camel:http Integration.java
 ```
 In this case, the dependency will be added with the correct version. Note that the standard notation for specifying a Camel dependency is `camel:xxx`, while `kamel` also accepts `camel-xxx` for usability.
 
+While resolving Camel dependencies (`camel:xxx` or `camel-xxx`) the Camel K operator tries to find the dependency in the xref:architecture/cr/camel-catalog.adoc[Camel catalog].
+In case the dependency is not listed in the catalog for some reason you will be provided with am error.
+Please make sure to use Camel dependencies listed in the catalog as these components are eligible to being used in Camel K (e.g. due to proper version resolving and runtime optimization).
+Using Camel dependencies not listed in the catalog may lead to unexpected behavior and is not supported.
+In case you do have a custom Camel component that you want to use as part of an Integration you can add this as an external Maven dependency using the respective Maven coordinates of your project.
+Please do not use one of the reserved the Camel groupIds (`org.apache.camel`) in that case.
+
 *External dependencies* can be added using the `-d` flag, the `mvn` prefix, and the maven coordinates:
 ```
 kamel run -d mvn:com.google.guava:guava:26.0-jre Integration.java
diff --git a/pkg/cmd/local/local.go b/pkg/cmd/local/local.go
index b1513a855..2955f0ed6 100644
--- a/pkg/cmd/local/local.go
+++ b/pkg/cmd/local/local.go
@@ -57,7 +57,7 @@ func GetDependencies(ctx context.Context, cmd *cobra.Command, srcs, userDependen
 	}
 
 	// Validate user-provided dependencies against Camel catalog
-	camel.ValidateDependencies(catalog, userDependencies, cmd)
+	camel.ValidateDependencies(catalog, userDependencies, cmd.ErrOrStderr())
 
 	// Get top-level dependencies from sources
 	dependencies, err := getTopLevelDependencies(ctx, catalog, srcs)
diff --git a/pkg/cmd/run_support.go b/pkg/cmd/run_support.go
index 812c7d326..985114387 100644
--- a/pkg/cmd/run_support.go
+++ b/pkg/cmd/run_support.go
@@ -42,7 +42,7 @@ import (
 
 func addDependency(cmd *cobra.Command, it *v1.Integration, dependency string, catalog *camel.RuntimeCatalog) {
 	normalized := camel.NormalizeDependency(dependency)
-	camel.ValidateDependency(catalog, normalized, cmd)
+	camel.ValidateDependency(catalog, normalized, cmd.ErrOrStderr())
 	it.Spec.AddDependency(normalized)
 }
 
diff --git a/pkg/util/camel/camel_dependencies.go b/pkg/util/camel/camel_dependencies.go
index a3b65b003..0fd7be5b5 100644
--- a/pkg/util/camel/camel_dependencies.go
+++ b/pkg/util/camel/camel_dependencies.go
@@ -49,53 +49,65 @@ func NormalizeDependency(dependency string) string {
 	return newDep
 }
 
-type Output interface {
-	OutOrStdout() io.Writer
-	ErrOrStderr() io.Writer
-}
-
 // ValidateDependencies validates dependencies against Camel catalog.
 // It only shows warning and does not throw error in case the Catalog is just not complete
 // and we don't want to let it stop the process.
-func ValidateDependencies(catalog *RuntimeCatalog, dependencies []string, out Output) {
+func ValidateDependencies(catalog *RuntimeCatalog, dependencies []string, out io.Writer) {
 	for _, d := range dependencies {
 		ValidateDependency(catalog, d, out)
 	}
 }
 
 // ValidateDependency validates a dependency against Camel catalog.
-// It only shows warning and does not throw error in case the Catalog is just not complete
+// It only shows warning and does not throw error in case the Catalog is just not complete,
 // and we don't want to let it stop the process.
-func ValidateDependency(catalog *RuntimeCatalog, dependency string, out Output) {
+func ValidateDependency(catalog *RuntimeCatalog, dependency string, out io.Writer) {
+	if err := ValidateDependencyE(catalog, dependency); err != nil {
+		fmt.Fprintf(out, "Warning: %s\n", err.Error())
+	}
+
 	switch {
-	case strings.HasPrefix(dependency, "camel:"):
-		artifact := strings.TrimPrefix(dependency, "camel:")
-		if ok := catalog.IsValidArtifact(artifact); !ok {
-			fmt.Fprintf(out.ErrOrStderr(), "Warning: dependency %s not found in Camel catalog\n", dependency)
-		}
 	case strings.HasPrefix(dependency, "mvn:org.apache.camel:"):
 		component := strings.Split(dependency, ":")[2]
-		fmt.Fprintf(out.ErrOrStderr(), "Warning: do not use %s. Use %s instead\n",
+		fmt.Fprintf(out, "Warning: do not use %s. Use %s instead\n",
 			dependency, NormalizeDependency(component))
 	case strings.HasPrefix(dependency, "mvn:org.apache.camel.quarkus:"):
 		component := strings.Split(dependency, ":")[2]
-		fmt.Fprintf(out.ErrOrStderr(), "Warning: do not use %s. Use %s instead\n",
+		fmt.Fprintf(out, "Warning: do not use %s. Use %s instead\n",
 			dependency, NormalizeDependency(component))
 	}
+}
 
+// ValidateDependencyE validates a dependency against Camel catalog and throws error
+// in case it does not exist in the catalog.
+func ValidateDependencyE(catalog *RuntimeCatalog, dependency string) error {
+	var artifact string
+	switch {
+	case strings.HasPrefix(dependency, "camel:"):
+		artifact = strings.TrimPrefix(dependency, "camel:")
+	case strings.HasPrefix(dependency, "camel-quarkus:"):
+		artifact = strings.TrimPrefix(dependency, "camel-quarkus:")
+	case strings.HasPrefix(dependency, "camel-"):
+		artifact = dependency
+	}
+
+	if artifact == "" {
+		return nil
+	}
+
+	if ok := catalog.IsValidArtifact(artifact); !ok {
+		return fmt.Errorf("dependency %s not found in Camel catalog", dependency)
+	}
+
+	return nil
 }
 
 // ValidateDependenciesE validates dependencies against Camel catalog and throws error
-// if it doesn't exist in the catalog.
+// in case it does not exist in the catalog.
 func ValidateDependenciesE(catalog *RuntimeCatalog, dependencies []string) error {
 	for _, dependency := range dependencies {
-		if !strings.HasPrefix(dependency, "camel:") {
-			continue
-		}
-
-		artifact := strings.TrimPrefix(dependency, "camel:")
-		if ok := catalog.IsValidArtifact(artifact); !ok {
-			return fmt.Errorf("dependency %s not found in Camel catalog", dependency)
+		if err := ValidateDependencyE(catalog, dependency); err != nil {
+			return err
 		}
 	}
 


Test patch:
diff --git a/pkg/util/camel/camel_dependencies_test.go b/pkg/util/camel/camel_dependencies_test.go
index 8bb78f3da..4469e0c17 100644
--- a/pkg/util/camel/camel_dependencies_test.go
+++ b/pkg/util/camel/camel_dependencies_test.go
@@ -18,8 +18,11 @@ limitations under the License.
 package camel
 
 import (
+	"fmt"
+	"strings"
 	"testing"
 
+	"github.com/apache/camel-k/pkg/util/maven"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -30,4 +33,129 @@ func TestNormalizeDependency(t *testing.T) {
 	assert.Equal(t, "camel:file", NormalizeDependency("camel-quarkus:file"))
 	assert.Equal(t, "camel-k:knative", NormalizeDependency("camel-k-knative"))
 	assert.Equal(t, "camel-k:knative", NormalizeDependency("camel-k:knative"))
+	assert.Equal(t, "mvn:org.apache.camel:camel-file", NormalizeDependency("mvn:org.apache.camel:camel-file"))
+	assert.Equal(t, "mvn:org.apache.camel.quarkus:camel-quarkus-file", NormalizeDependency("mvn:org.apache.camel.quarkus:camel-quarkus-file"))
+	assert.Equal(t, "mvn:org.apache.camel:camel-k-knative", NormalizeDependency("mvn:org.apache.camel:camel-k-knative"))
+}
+
+func TestValidateDependency(t *testing.T) {
+	catalog, err := DefaultCatalog()
+	assert.Nil(t, err)
+
+	output := strings.Builder{}
+	ValidateDependency(catalog, "", &output)
+	assert.Equal(t, "", output.String())
+
+	output.Reset()
+	ValidateDependency(catalog, "camel:file", &output)
+	assert.Equal(t, "", output.String())
+
+	output.Reset()
+	ValidateDependency(catalog, "camel-quarkus-file", &output)
+	assert.Equal(t, "", output.String())
+
+	output.Reset()
+	ValidateDependency(catalog, "camel-quarkus:file", &output)
+	assert.Equal(t, "", output.String())
+
+	output.Reset()
+	ValidateDependency(catalog, "camel:unknown", &output)
+	assert.Equal(t, "Warning: dependency camel:unknown not found in Camel catalog\n", output.String())
+
+	output.Reset()
+	ValidateDependency(catalog, "mvn:org.apache.camel:camel-foo", &output)
+	assert.Equal(t, "Warning: do not use mvn:org.apache.camel:camel-foo. Use camel:foo instead\n", output.String())
+
+	output.Reset()
+	ValidateDependency(catalog, "mvn:org.apache.camel.quarkus:camel-quarkus-foo", &output)
+	assert.Equal(t, "Warning: do not use mvn:org.apache.camel.quarkus:camel-quarkus-foo. Use camel:foo instead\n", output.String())
+}
+
+func TestManageIntegrationDependencies(t *testing.T) {
+	catalog, err := DefaultCatalog()
+	assert.Nil(t, err)
+
+	tests := []struct {
+		name         string
+		dependencies []string
+		coordinates  string
+	}{
+		{
+			name: "basic_camel",
+			dependencies: []string{
+				"camel:direct",
+				"camel:log",
+				"camel:core",
+			},
+			coordinates: "org.apache.camel.quarkus:camel-quarkus-direct," +
+				"org.apache.camel.quarkus:camel-quarkus-log," +
+				"org.apache.camel.quarkus:camel-quarkus-core",
+		},
+		{
+			name: "camel_quarkus",
+			dependencies: []string{
+				"camel:direct",
+				"camel-quarkus:log",
+				"camel:camel-quarkus-core",
+			},
+			coordinates: "org.apache.camel.quarkus:camel-quarkus-direct," +
+				"org.apache.camel.quarkus:camel-quarkus-log," +
+				"org.apache.camel.quarkus:camel-quarkus-core",
+		},
+		{
+			name: "camel_k",
+			dependencies: []string{
+				"camel:direct",
+				"camel-k:webhook",
+			},
+			coordinates: "org.apache.camel.quarkus:camel-quarkus-direct," +
+				"org.apache.camel.k:camel-k-webhook",
+		},
+		{
+			name: "not_in_catalog",
+			dependencies: []string{
+				"camel:direct",
+				"camel:resiliance4j",
+			},
+			coordinates: "org.apache.camel.quarkus:camel-quarkus-direct," +
+				"org.apache.camel.quarkus:camel-quarkus-resiliance4j",
+		},
+		{
+			name: "mvn",
+			dependencies: []string{
+				"mvn:org.foo:bar",
+				"mvn:org.apache.camel:camel-resiliance4j",
+			},
+			coordinates: "org.foo:bar," +
+				"org.apache.camel:camel-resiliance4j",
+		},
+		{
+			name: "jitpack",
+			dependencies: []string{
+				"github:apache/camel-sample/1.0",
+			},
+			coordinates: "com.github.apache:camel-sample",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			project := maven.Project{}
+
+			err = ManageIntegrationDependencies(&project, test.dependencies, catalog)
+			assert.Nil(t, err)
+
+			coordinates := strings.Builder{}
+			for i, d := range project.Dependencies {
+				if i == 0 {
+					_, err = fmt.Fprintf(&coordinates, "%s:%s", d.GroupID, d.ArtifactID)
+					assert.Nil(t, err)
+				} else {
+					_, err = fmt.Fprintf(&coordinates, ",%s:%s", d.GroupID, d.ArtifactID)
+					assert.Nil(t, err)
+				}
+			}
+			assert.Equal(t, test.coordinates, coordinates.String(), coordinates)
+		})
+	}
 }