You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by pc...@apache.org on 2022/01/07 13:48:21 UTC

[camel-k] 15/24: feat(cmd/run): convert openapi to trait

This is an automated email from the ASF dual-hosted git repository.

pcongiusti pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-k.git

commit a73dce39ce2ceec9dc55492d518caac042812b67
Author: Pasquale Congiusti <pa...@gmail.com>
AuthorDate: Tue Dec 7 16:22:04 2021 +0100

    feat(cmd/run): convert openapi to trait
---
 e2e/common/traits/files/openapi/petstore-api.yaml | 128 ++++++++++++++++++++++
 e2e/common/traits/files/openapi/petstore.groovy   |  29 +++++
 e2e/common/traits/openapi_test.go                 |  69 ++++++++++++
 pkg/apis/camel/v1/integration_types.go            |  11 +-
 pkg/cmd/run.go                                    |  92 +++++++---------
 pkg/cmd/run_test.go                               |  17 ++-
 pkg/trait/openapi.go                              |  12 +-
 7 files changed, 296 insertions(+), 62 deletions(-)

diff --git a/e2e/common/traits/files/openapi/petstore-api.yaml b/e2e/common/traits/files/openapi/petstore-api.yaml
new file mode 100644
index 0000000..1b6d69c
--- /dev/null
+++ b/e2e/common/traits/files/openapi/petstore-api.yaml
@@ -0,0 +1,128 @@
+# ---------------------------------------------------------------------------
+# 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.
+# ---------------------------------------------------------------------------
+
+openapi: "3.0.0"
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+  license:
+    name: MIT
+servers:
+  - url: http://petstore.swagger.io/v1
+paths:
+  /pets:
+    get:
+      summary: List all pets
+      operationId: listPets
+      tags:
+        - pets
+      parameters:
+        - name: limit
+          in: query
+          description: How many items to return at one time (max 100)
+          required: false
+          schema:
+            type: integer
+            format: int32
+      responses:
+        '200':
+          description: A paged array of pets
+          headers:
+            x-next:
+              description: A link to the next page of responses
+              schema:
+                type: string
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Pets"
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Error"
+    post:
+      summary: Create a pet
+      operationId: createPets
+      tags:
+        - pets
+      responses:
+        '201':
+          description: Null response
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Error"
+  /pets/{petId}:
+    get:
+      summary: Info for a specific pet
+      operationId: showPetById
+      tags:
+        - pets
+      parameters:
+        - name: petId
+          in: path
+          required: true
+          description: The id of the pet to retrieve
+          schema:
+            type: string
+      responses:
+        '200':
+          description: Expected response to a valid request
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Pet"
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Error"
+components:
+  schemas:
+    Pet:
+      type: object
+      required:
+        - id
+        - name
+      properties:
+        id:
+          type: integer
+          format: int64
+        name:
+          type: string
+        tag:
+          type: string
+    Pets:
+      type: array
+      items:
+        $ref: "#/components/schemas/Pet"
+    Error:
+      type: object
+      required:
+        - code
+        - message
+      properties:
+        code:
+          type: integer
+          format: int32
+        message:
+          type: string
diff --git a/e2e/common/traits/files/openapi/petstore.groovy b/e2e/common/traits/files/openapi/petstore.groovy
new file mode 100644
index 0000000..d382b21
--- /dev/null
+++ b/e2e/common/traits/files/openapi/petstore.groovy
@@ -0,0 +1,29 @@
+// camel-k: language=groovy
+/*
+ * 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.
+ */
+
+//
+//  kamel run --dev --name petstore --open-api examples/petstore-api.yaml examples/petstore.groovy
+// 
+
+from('direct:listPets')
+    .log('listPets')
+from('direct:createPets')
+    .log('createPets')
+from('direct:showPetById')
+    .log('showPetById')
+
diff --git a/e2e/common/traits/openapi_test.go b/e2e/common/traits/openapi_test.go
new file mode 100644
index 0000000..7318ec7
--- /dev/null
+++ b/e2e/common/traits/openapi_test.go
@@ -0,0 +1,69 @@
+//go:build integration
+// +build integration
+
+// To enable compilation of this file in Goland, go to "Settings -> Go -> Vendoring & Build Tags -> Custom Tags" and add "integration"
+
+/*
+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 knative
+
+import (
+	"io/ioutil"
+	"testing"
+
+	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/assert"
+
+	corev1 "k8s.io/api/core/v1"
+
+	. "github.com/apache/camel-k/e2e/support"
+)
+
+func TestOpenAPIConfigmap(t *testing.T) {
+	WithNewTestNamespace(t, func(ns string) {
+		Expect(Kamel("install", "-n", ns).Execute()).To(Succeed())
+
+		openapiContent, err := ioutil.ReadFile("./files/openapi/petstore-api.yaml")
+		assert.Nil(t, err)
+		var cmDataProps = make(map[string]string)
+		cmDataProps["petstore-api.yaml"] = string(openapiContent)
+		NewPlainTextConfigmap(ns, "my-openapi", cmDataProps)
+
+		Expect(Kamel(
+			"run",
+			"-n", ns,
+			"--name", "petstore",
+			"--open-api", "configmap:my-openapi",
+			"files/openapi/petstore.groovy",
+		).Execute()).To(Succeed())
+
+		Eventually(IntegrationPodPhase(ns, "petstore"), TestTimeoutLong).
+			Should(Equal(corev1.PodRunning))
+		Eventually(Deployment(ns, "petstore"), TestTimeoutLong).
+			Should(Not(BeNil()))
+
+		Eventually(IntegrationLogs(ns, "petstore"), TestTimeoutMedium).
+			Should(ContainSubstring("Started listPets (rest://get:/v1:/pets)"))
+		Eventually(IntegrationLogs(ns, "petstore"), TestTimeoutMedium).
+			Should(ContainSubstring("Started createPets (rest://post:/v1:/pets)"))
+		Eventually(IntegrationLogs(ns, "petstore"), TestTimeoutMedium).
+			Should(ContainSubstring("Started showPetById (rest://get:/v1:/pets/%7BpetId%7D)"))
+
+		Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
+	})
+}
diff --git a/pkg/apis/camel/v1/integration_types.go b/pkg/apis/camel/v1/integration_types.go
index 71b38ec..9f9686a 100644
--- a/pkg/apis/camel/v1/integration_types.go
+++ b/pkg/apis/camel/v1/integration_types.go
@@ -27,10 +27,13 @@ import (
 
 // IntegrationSpec defines the desired state of Integration
 type IntegrationSpec struct {
-	Replicas  *int32         `json:"replicas,omitempty"`
-	Sources   []SourceSpec   `json:"sources,omitempty"`
-	Flows     []Flow         `json:"flows,omitempty"`
-	Resources []ResourceSpec `json:"resources,omitempty"`
+	Replicas *int32       `json:"replicas,omitempty"`
+	Sources  []SourceSpec `json:"sources,omitempty"`
+	Flows    []Flow       `json:"flows,omitempty"`
+	// Deprecated:
+	// Use container trait (container.resources) to manage resources
+	// Use openapi trait (openapi.configmaps) to manage OpenAPIs specifications
+	Resources []ResourceSpec `json:"resources,deprecatedInFavorOf,omitempty,deprecated"`
 	// Deprecated: use the IntegrationKit field
 	Kit            string                  `json:"kit,omitempty"`
 	IntegrationKit *corev1.ObjectReference `json:"integrationKit,omitempty"`
diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go
index 771215e..1c1b183 100644
--- a/pkg/cmd/run.go
+++ b/pkg/cmd/run.go
@@ -23,7 +23,6 @@ import (
 	"fmt"
 	"os"
 	"os/signal"
-	"path"
 	"reflect"
 	"regexp"
 	"strings"
@@ -92,7 +91,7 @@ func newCmdRun(rootCmdOptions *RootCmdOptions) (*cobra.Command, *runCmdOptions)
 	cmd.Flags().StringArrayP("trait", "t", nil, "Configure a trait. E.g. \"-t service.enabled=false\"")
 	cmd.Flags().StringP("output", "o", "", "Output format. One of: json|yaml")
 	cmd.Flags().Bool("compression", false, "Enable storage of sources and resources as a compressed binary blobs")
-	cmd.Flags().StringArray("open-api", nil, "Add an OpenAPI v2 spec")
+	cmd.Flags().StringArray("open-api", nil, "Add an OpenAPI spec (syntax: [configmap|file]:name)")
 	cmd.Flags().StringArrayP("volume", "v", nil, "Mount a volume into the integration container. E.g \"-v pvcname:/container/path\"")
 	cmd.Flags().StringArrayP("env", "e", nil, "Set an environment variable in the integration container. E.g \"-e MY_VAR=my-value\"")
 	cmd.Flags().StringArray("property-file", nil, "[Deprecated] Bind a property file to the integration. E.g. \"--property-file integration.properties\"")
@@ -262,6 +261,13 @@ func (o *runCmdOptions) validate() error {
 		}
 	}
 
+	for _, openapi := range o.OpenAPIs {
+		// We support only local file and cluster configmaps
+		if !(strings.HasPrefix(openapi, "file:") || strings.HasPrefix(openapi, "configmap:")) {
+			return fmt.Errorf(`invalid openapi specification "%s". It supports only file or configmap`, openapi)
+		}
+	}
+
 	return nil
 }
 
@@ -417,7 +423,7 @@ func (o *runCmdOptions) syncIntegration(cmd *cobra.Command, c client.Client, sou
 	files = append(files, filterFileLocation(o.Properties)...)
 	files = append(files, filterFileLocation(o.BuildProperties)...)
 	files = append(files, o.PropertyFiles...)
-	files = append(files, o.OpenAPIs...)
+	files = append(files, filterFileLocation(o.OpenAPIs)...)
 
 	for _, s := range files {
 		ok, err := isLocalAndFileExists(s)
@@ -570,42 +576,21 @@ func (o *runCmdOptions) createOrUpdateIntegration(cmd *cobra.Command, c client.C
 	}
 
 	generatedConfigmaps := make([]*corev1.ConfigMap, 0)
-	for _, res := range o.Resources {
-		config, err := resource.ParseResource(res)
-		if err != nil {
-			return nil, err
-		}
-		// We try to autogenerate a configmap
-		maybeGenCm, err := parseConfigAndGenCm(o.Context, c, config, integration, o.Compression)
-		if err != nil {
-			return nil, err
-		}
-		if maybeGenCm != nil {
-			generatedConfigmaps = append(generatedConfigmaps, maybeGenCm)
-		}
-		o.Traits = append(o.Traits, convertToTrait(config.String(), "container.resources"))
+	resCms, err := o.parseAndConvertToTrait(c, integration, o.Resources, resource.ParseResource, func(c *resource.Config) string { return c.String() }, "container.resources")
+	if err != nil {
+		return nil, err
 	}
-	for _, conf := range o.Configs {
-		config, err := resource.ParseResource(conf)
-		if err != nil {
-			return nil, err
-		}
-		// We try to autogenerate a configmap
-		maybeGenCm, err := parseConfigAndGenCm(o.Context, c, config, integration, o.Compression)
-		if err != nil {
-			return nil, err
-		}
-		if maybeGenCm != nil {
-			generatedConfigmaps = append(generatedConfigmaps, maybeGenCm)
-		}
-		o.Traits = append(o.Traits, convertToTrait(config.String(), "container.configs"))
+	generatedConfigmaps = append(generatedConfigmaps, resCms...)
+	confCms, err := o.parseAndConvertToTrait(c, integration, o.Configs, resource.ParseConfig, func(c *resource.Config) string { return c.String() }, "container.configs")
+	if err != nil {
+		return nil, err
 	}
-
-	for _, resource := range o.OpenAPIs {
-		if err = addResource(o.Context, resource, &integration.Spec, o.Compression, v1.ResourceTypeOpenAPI); err != nil {
-			return nil, err
-		}
+	generatedConfigmaps = append(generatedConfigmaps, confCms...)
+	oAPICms, err := o.parseAndConvertToTrait(c, integration, o.OpenAPIs, resource.ParseConfig, func(c *resource.Config) string { return c.Name() }, "openapi.configmaps")
+	if err != nil {
+		return nil, err
 	}
+	generatedConfigmaps = append(generatedConfigmaps, oAPICms...)
 
 	for _, item := range o.Dependencies {
 		integration.Spec.AddDependency(item)
@@ -703,21 +688,28 @@ func (o *runCmdOptions) createOrUpdateIntegration(cmd *cobra.Command, c client.C
 	return integration, nil
 }
 
-func addResource(ctx context.Context, resourceLocation string, integrationSpec *v1.IntegrationSpec, enableCompression bool, resourceType v1.ResourceType) error {
-	if data, _, compressed, err := loadTextContent(ctx, resourceLocation, enableCompression); err == nil {
-		integrationSpec.AddResources(v1.ResourceSpec{
-			DataSpec: v1.DataSpec{
-				Name:        path.Base(resourceLocation),
-				Content:     data,
-				Compression: compressed,
-			},
-			Type: resourceType,
-		})
-	} else {
-		return err
+func (o *runCmdOptions) parseAndConvertToTrait(
+	c client.Client, integration *v1.Integration, params []string,
+	parse func(string) (*resource.Config, error),
+	convert func(*resource.Config) string,
+	traitParam string) ([]*corev1.ConfigMap, error) {
+	generatedCms := make([]*corev1.ConfigMap, 0)
+	for _, param := range params {
+		config, err := parse(param)
+		if err != nil {
+			return nil, err
+		}
+		// We try to autogenerate a configmap
+		maybeGenCm, err := parseConfigAndGenCm(o.Context, c, config, integration, o.Compression)
+		if err != nil {
+			return nil, err
+		}
+		if maybeGenCm != nil {
+			generatedCms = append(generatedCms, maybeGenCm)
+		}
+		o.Traits = append(o.Traits, convertToTrait(convert(config), traitParam))
 	}
-
-	return nil
+	return generatedCms, nil
 }
 
 func convertToTrait(value, traitParameter string) string {
diff --git a/pkg/cmd/run_test.go b/pkg/cmd/run_test.go
index 67c0453..aa25816 100644
--- a/pkg/cmd/run_test.go
+++ b/pkg/cmd/run_test.go
@@ -190,13 +190,22 @@ func TestRunNameFlag(t *testing.T) {
 func TestRunOpenApiFlag(t *testing.T) {
 	runCmdOptions, rootCmd, _ := initializeRunCmdOptions(t)
 	_, err := test.ExecuteCommand(rootCmd, cmdRun,
-		"--open-api", "oapi1",
-		"--open-api", "oapi2",
+		"--open-api", "file:oapi1",
+		"--open-api", "configmap:oapi2",
 		integrationSource)
 	assert.Nil(t, err)
 	assert.Len(t, runCmdOptions.OpenAPIs, 2)
-	assert.Equal(t, "oapi1", runCmdOptions.OpenAPIs[0])
-	assert.Equal(t, "oapi2", runCmdOptions.OpenAPIs[1])
+	assert.Equal(t, "file:oapi1", runCmdOptions.OpenAPIs[0])
+	assert.Equal(t, "configmap:oapi2", runCmdOptions.OpenAPIs[1])
+}
+
+func TestRunOpenApiInvalidFlag(t *testing.T) {
+	_, rootCmd, _ := initializeRunCmdOptions(t)
+	_, err := test.ExecuteCommand(rootCmd, cmdRun,
+		"--open-api", "secret:oapi1",
+		"--open-api", "oapi2",
+		integrationSource)
+	assert.NotNil(t, err)
 }
 
 func TestRunOutputFlag(t *testing.T) {
diff --git a/pkg/trait/openapi.go b/pkg/trait/openapi.go
index b1ff98d..43c8791 100644
--- a/pkg/trait/openapi.go
+++ b/pkg/trait/openapi.go
@@ -100,21 +100,22 @@ func (t *openAPITrait) Apply(e *Environment) error {
 	if err != nil {
 		return err
 	}
+	defer os.RemoveAll(tmpDir)
 
 	generatedFromResources, err := t.generateFromResources(e, tmpDir)
 	if err != nil {
-		return os.RemoveAll(tmpDir)
+		return err
 	}
 	generatedFromConfigmaps, err := t.generateFromConfigmaps(e, tmpDir)
 	if err != nil {
-		return os.RemoveAll(tmpDir)
+		return err
 	}
-	if generatedFromConfigmaps != nil && len(generatedFromConfigmaps) > 0 {
+	if len(generatedFromConfigmaps) > 0 {
 		generatedFromResources = append(generatedFromResources, generatedFromConfigmaps...)
 	}
 	e.Integration.Status.GeneratedSources = generatedFromResources
 
-	return os.RemoveAll(tmpDir)
+	return nil
 }
 
 func (t *openAPITrait) generateFromResources(e *Environment, tmpDir string) ([]v1.SourceSpec, error) {
@@ -136,6 +137,9 @@ func (t *openAPITrait) generateFromConfigmaps(e *Environment, tmpDir string) ([]
 	dataSpecs := make([]v1.DataSpec, 0, len(t.Configmaps))
 	for _, configmap := range t.Configmaps {
 		cm := kubernetes.LookupConfigmap(e.Ctx, e.Client, e.Integration.Namespace, configmap)
+		if cm == nil {
+			return nil, fmt.Errorf("could not find any configmap with name: %s", configmap)
+		}
 		// Iterate over each configmap key which may hold a different OpenAPI spec
 		for k, v := range cm.Data {
 			dataSpecs = append(dataSpecs, v1.DataSpec{