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 <[email protected]> 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{
