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
commit 7a9b7f896fd9e975c0c077de7223eae49eff5222 Author: Nicola Ferraro <ni.ferr...@gmail.com> AuthorDate: Thu May 7 15:05:37 2020 +0200 Fix #1449: add support for modeline options --- cmd/kamel/main.go | 16 ++++- pkg/cmd/modeline.go | 130 +++++++++++++++++++++++++++++++++ pkg/cmd/modeline_test.go | 119 ++++++++++++++++++++++++++++++ pkg/cmd/run.go | 8 +-- pkg/util/modeline/parser.go | 84 ++++++++++++++++++++++ pkg/util/modeline/parser_test.go | 152 +++++++++++++++++++++++++++++++++++++++ pkg/util/modeline/types.go | 7 ++ 7 files changed, 510 insertions(+), 6 deletions(-) diff --git a/cmd/kamel/main.go b/cmd/kamel/main.go index 2ba8ea9..be385b4 100644 --- a/cmd/kamel/main.go +++ b/cmd/kamel/main.go @@ -19,6 +19,7 @@ package main import ( "context" + "fmt" "math/rand" "os" "time" @@ -40,8 +41,19 @@ func main() { // Cancel ctx as soon as main returns defer cancel() - rootCmd, err := cmd.NewKamelCommand(ctx) - exitOnError(err) + // Add modeline options to the command + rootCmd, args, err := cmd.NewKamelWithModelineCommand(ctx, os.Args) + if err != nil { + fmt.Printf("Error: %s\n", err.Error()) + exitOnError(err) + } + + // Give a feedback about the actual command that is run + fmt.Fprint(rootCmd.OutOrStdout(), "Executing: kamel ") + for _, a := range args { + fmt.Fprintf(rootCmd.OutOrStdout(), "%s ", a) + } + fmt.Fprintln(rootCmd.OutOrStdout()) err = rootCmd.Execute() exitOnError(err) diff --git a/pkg/cmd/modeline.go b/pkg/cmd/modeline.go new file mode 100644 index 0000000..60343ca --- /dev/null +++ b/pkg/cmd/modeline.go @@ -0,0 +1,130 @@ +package cmd + +import ( + "context" + "fmt" + "path" + + "github.com/apache/camel-k/pkg/util/modeline" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "path/filepath" +) + +const ( + runCmdName = "run" + runCmdSourcesArgs = "source" +) + +var ( + nonRunOptions = map[string]bool{ + "language": true, // language is a marker modeline option for other tools + } + + // file options must be considered relative to the source files they belong to + fileOptions = map[string]bool{ + "source": true, + "resource": true, + "config": true, + "open-api": true, + "property-file": true, + } +) + +func NewKamelWithModelineCommand(ctx context.Context, osArgs []string) (*cobra.Command, []string, error) { + processed := make(map[string]bool) + return createKamelWithModelineCommand(ctx, osArgs[1:], processed) +} + +func createKamelWithModelineCommand(ctx context.Context, args []string, processedFiles map[string]bool) (*cobra.Command, []string, error) { + rootCmd, err := NewKamelCommand(ctx) + if err != nil { + return nil, nil, err + } + + target, flags, err := rootCmd.Find(args) + if err != nil { + return nil, nil, err + } + + if target.Name() != runCmdName { + return rootCmd, args, nil + } + + err = target.ParseFlags(flags) + if err != nil { + return nil, nil, err + } + + fg := target.Flags() + + sources, err := fg.GetStringArray(runCmdSourcesArgs) + if err != nil { + return nil, nil, err + } + + var files = append([]string(nil), fg.Args()...) + files = append(files, sources...) + + var opts []modeline.Option + for _, f := range files { + if processedFiles[f] { + continue + } + baseDir := filepath.Dir(f) + content, err := loadData(f, false) + if err != nil { + return nil, nil, errors.Wrapf(err, "cannot read file %s", f) + } + ops, err := modeline.Parse(f, content) + if err != nil { + return nil, nil, errors.Wrapf(err, "cannot process file %s", f) + } + for i, o := range ops { + if fileOptions[o.Name] && !isRemoteHTTPFile(f) { + refPath := o.Value + if !filepath.IsAbs(refPath) { + full := path.Join(baseDir, refPath) + o.Value = full + ops[i] = o + } + } + } + opts = append(opts, ops...) + } + // filter out in place non-run options + nOpts := 0 + for _, o := range opts { + if !nonRunOptions[o.Name] { + opts[nOpts] = o + nOpts++ + } + } + opts = opts[:nOpts] + + // No new options, returning a new command with computed args + if len(opts) == 0 { + // Recreating the command as it's dirty + rootCmd, err = NewKamelCommand(ctx) + if err != nil { + return nil, nil, err + } + rootCmd.SetArgs(args) + return rootCmd, args, nil + } + + // New options added, recomputing + for _, f := range files { + processedFiles[f] = true + } + for _, o := range opts { + prefix := "-" + if len(o.Name) > 1 { + prefix = "--" + } + args = append(args, fmt.Sprintf("%s%s", prefix, o.Name)) + args = append(args, o.Value) + } + + return createKamelWithModelineCommand(ctx, args, processedFiles) +} diff --git a/pkg/cmd/modeline_test.go b/pkg/cmd/modeline_test.go new file mode 100644 index 0000000..6eabd18 --- /dev/null +++ b/pkg/cmd/modeline_test.go @@ -0,0 +1,119 @@ +/* +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 ( + "context" + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "io/ioutil" +) + +func TestModelineRunSimple(t *testing.T) { + dir, err := ioutil.TempDir("", "camel-k-test-") + assert.NoError(t, err) + defer os.RemoveAll(dir) + + file := ` + // camel-k: dependency=mvn:org.my:lib:1.0 + ` + fileName := path.Join(dir, "simple.groovy") + err = ioutil.WriteFile(fileName, []byte(file), 0777) + assert.NoError(t, err) + + cmd, flags, err := NewKamelWithModelineCommand(context.TODO(), []string{"kamel", "run", fileName}) + assert.NoError(t, err) + assert.NotNil(t, cmd) + assert.Equal(t, []string{"run", fileName, "--dependency", "mvn:org.my:lib:1.0"}, flags) +} + +func TestModelineRunChain(t *testing.T) { + dir, err := ioutil.TempDir("", "camel-k-test-") + assert.NoError(t, err) + defer os.RemoveAll(dir) + + file := ` + // camel-k: dependency=mvn:org.my:lib:1.0 + ` + fileName := path.Join(dir, "simple.groovy") + err = ioutil.WriteFile(fileName, []byte(file), 0777) + assert.NoError(t, err) + + cmd, flags, err := NewKamelWithModelineCommand(context.TODO(), []string{"kamel", "run", "-d", "mvn:org.my:lib2:1.0", fileName}) + assert.NoError(t, err) + assert.NotNil(t, cmd) + assert.Equal(t, []string{"run", "-d", "mvn:org.my:lib2:1.0", fileName, "--dependency", "mvn:org.my:lib:1.0"}, flags) +} + +func TestModelineRunMultipleFiles(t *testing.T) { + dir, err := ioutil.TempDir("", "camel-k-test-") + assert.NoError(t, err) + defer os.RemoveAll(dir) + + file := ` + // camel-k: source=ext.groovy + ` + fileName := path.Join(dir, "simple.groovy") + err = ioutil.WriteFile(fileName, []byte(file), 0777) + assert.NoError(t, err) + + file2 := ` + // camel-k: dependency=mvn:org.my:lib:1.0 + ` + fileName2 := path.Join(dir, "ext.groovy") + err = ioutil.WriteFile(fileName2, []byte(file2), 0777) + assert.NoError(t, err) + + cmd, flags, err := NewKamelWithModelineCommand(context.TODO(), []string{"kamel", "run", fileName}) + assert.NoError(t, err) + assert.NotNil(t, cmd) + assert.Equal(t, []string{"run", fileName, "--source", fileName2, "--dependency", "mvn:org.my:lib:1.0"}, flags) +} + +func TestModelineRunPropertyFiles(t *testing.T) { + dir, err := ioutil.TempDir("", "camel-k-test-") + assert.NoError(t, err) + defer os.RemoveAll(dir) + + subDir := path.Join(dir, "sub") + err = os.Mkdir(subDir, 0777) + assert.NoError(t, err) + + file := ` + // camel-k: property-file=../application.properties + ` + fileName := path.Join(subDir, "simple.groovy") + err = ioutil.WriteFile(fileName, []byte(file), 0777) + assert.NoError(t, err) + + propFile := ` + a=b + ` + propFileName := path.Join(dir, "application.properties") + err = ioutil.WriteFile(propFileName, []byte(propFile), 0777) + assert.NoError(t, err) + + cmd, flags, err := NewKamelWithModelineCommand(context.TODO(), []string{"kamel", "run", fileName}) + assert.NoError(t, err) + assert.NotNil(t, cmd) + assert.Equal(t, []string{"run", fileName, "--property-file", propFileName}, flags) +} diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index f2ee010..66e3a88 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -470,7 +470,7 @@ func (o *runCmdOptions) updateIntegrationCode(c client.Client, sources []string) srcs = append(srcs, o.Sources...) for _, source := range srcs { - data, err := o.loadData(source, o.Compression) + data, err := loadData(source, o.Compression) if err != nil { return nil, err } @@ -485,7 +485,7 @@ func (o *runCmdOptions) updateIntegrationCode(c client.Client, sources []string) } for _, resource := range o.Resources { - data, err := o.loadData(resource, o.Compression) + data, err := loadData(resource, o.Compression) if err != nil { return nil, err } @@ -501,7 +501,7 @@ func (o *runCmdOptions) updateIntegrationCode(c client.Client, sources []string) } for _, resource := range o.OpenAPIs { - data, err := o.loadData(resource, o.Compression) + data, err := loadData(resource, o.Compression) if err != nil { return nil, err } @@ -623,7 +623,7 @@ func (o *runCmdOptions) GetIntegrationName(sources []string) string { return name } -func (*runCmdOptions) loadData(fileName string, compress bool) (string, error) { +func loadData(fileName string, compress bool) (string, error) { var content []byte var err error diff --git a/pkg/util/modeline/parser.go b/pkg/util/modeline/parser.go new file mode 100644 index 0000000..f1ce980 --- /dev/null +++ b/pkg/util/modeline/parser.go @@ -0,0 +1,84 @@ +package modeline + +import ( + "bufio" + "fmt" + "regexp" + "strings" + + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" +) + +var ( + commonModelineRegexp = regexp.MustCompile(`^\s*//\s*camel-k\s*:\s*([^\s]+.*)$`) + yamlModelineRegexp = regexp.MustCompile(`^\s*#+\s*camel-k\s*:\s*([^\s]+.*)$`) + xmlModelineRegexp = regexp.MustCompile(`^.*<!--\s*camel-k\s*:\s*([^\s]+[^>]*)-->.*$`) + + delimiter = regexp.MustCompile(`\s+`) +) + +func Parse(name, content string) (res []Option, err error) { + lang := inferLanguage(name) + if lang == "" { + return nil, fmt.Errorf("unsupported file type %s", name) + } + scanner := bufio.NewScanner(strings.NewReader(content)) + for scanner.Scan() { + res = append(res, getModelineOptions(scanner.Text(), lang)...) + } + return res, scanner.Err() +} + +func getModelineOptions(line string, lang v1.Language) (res []Option) { + reg := modelineRegexp(lang) + if !reg.MatchString(line) { + return nil + } + strs := reg.FindStringSubmatch(line) + if len(strs) == 2 { + tokens := delimiter.Split(strs[1], -1) + for _, token := range tokens { + if len(strings.Trim(token, "\t\n\f\r ")) == 0 { + continue + } + eq := strings.Index(token, "=") + var name, value string + if eq > 0 { + name = token[0:eq] + value = token[eq+1:] + } else { + name = token + value = "" + } + opt := Option{ + Name: name, + Value: value, + } + res = append(res, opt) + } + } + return res +} + +func modelineRegexp(lang v1.Language) *regexp.Regexp { + switch lang { + case v1.LanguageYaml: + return yamlModelineRegexp + case v1.LanguageXML: + return xmlModelineRegexp + default: + return commonModelineRegexp + } +} + +func inferLanguage(fileName string) v1.Language { + for _, l := range v1.Languages { + if strings.HasSuffix(fileName, fmt.Sprintf(".%s", string(l))) { + return l + } + } + if strings.HasSuffix(fileName, ".yml") { + return v1.LanguageYaml + } + return "" +} diff --git a/pkg/util/modeline/parser_test.go b/pkg/util/modeline/parser_test.go new file mode 100644 index 0000000..0a703db --- /dev/null +++ b/pkg/util/modeline/parser_test.go @@ -0,0 +1,152 @@ +package modeline + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestParseGroovyFile(t *testing.T) { + it := ` + // camel-k: pippo=pluto paperino ciao=1 + // camel-k : ciao + + from("timer:tick").log("Ciao") + ` + opts, err := Parse("simple.groovy", it) + assert.NoError(t, err) + assert.Len(t, opts, 4) + assert.Contains(t, opts, Option{Name: "pippo", Value: "pluto"}) + assert.Contains(t, opts, Option{Name: "paperino"}) + assert.Contains(t, opts, Option{Name: "ciao", Value: "1"}) + assert.Contains(t, opts, Option{Name: "ciao"}) +} + +func TestParseKotlinFile(t *testing.T) { + it := ` + // camel-k: pippo=pluto paperino ciao=1 + // camel-k : ciao + + from("timer:tick").log("Ciao") + ` + opts, err := Parse("example.kts", it) + assert.NoError(t, err) + assert.Len(t, opts, 4) + assert.Contains(t, opts, Option{Name: "pippo", Value: "pluto"}) + assert.Contains(t, opts, Option{Name: "paperino"}) + assert.Contains(t, opts, Option{Name: "ciao", Value: "1"}) + assert.Contains(t, opts, Option{Name: "ciao"}) +} + +func TestParseJavaFile(t *testing.T) { + it := ` + // camel-k: pippo=pluto paperino ciao=1 + // camel-k : ciao + + import org.apache.camel.builder.RouteBuilder; + + public class {{ .Name }} extends RouteBuilder { + @Override + public void configure() throws Exception { + + // Write your routes here, for example: + from("timer:java?period=1000") + .routeId("java") + .setBody() + .simple("Hello Camel K from ${routeId}") + .to("log:info"); + + } + } + ` + opts, err := Parse("Example.java", it) + assert.NoError(t, err) + assert.Len(t, opts, 4) + assert.Contains(t, opts, Option{Name: "pippo", Value: "pluto"}) + assert.Contains(t, opts, Option{Name: "paperino"}) + assert.Contains(t, opts, Option{Name: "ciao", Value: "1"}) + assert.Contains(t, opts, Option{Name: "ciao"}) +} + +func TestParseJSFile(t *testing.T) { + it := ` + // camel-k: pippo=pluto paperino ciao=1 + // camel-k : ciao + // Write your routes here, for example: + from('timer:js?period=1000') + .routeId('js') + .setBody() + .simple('Hello Camel K from ${routeId}') + .to('log:info') + ` + opts, err := Parse("example.js", it) + assert.NoError(t, err) + assert.Len(t, opts, 4) + assert.Contains(t, opts, Option{Name: "pippo", Value: "pluto"}) + assert.Contains(t, opts, Option{Name: "paperino"}) + assert.Contains(t, opts, Option{Name: "ciao", Value: "1"}) + assert.Contains(t, opts, Option{Name: "ciao"}) +} + +func TestParseYAMLFile(t *testing.T) { + it := ` + # camel-k: pippo=pluto paperino ciao=1 + ### camel-k : ciao + + # Write your routes here, for example: + - from: + uri: "timer:yaml" + parameters: + period: "1000" + steps: + - set-body: + constant: "Hello Camel K from yaml" + - to: "log:info" + + ` + opts, err := Parse("example.yaml", it) + assert.NoError(t, err) + assert.Len(t, opts, 4) + assert.Contains(t, opts, Option{Name: "pippo", Value: "pluto"}) + assert.Contains(t, opts, Option{Name: "paperino"}) + assert.Contains(t, opts, Option{Name: "ciao", Value: "1"}) + assert.Contains(t, opts, Option{Name: "ciao"}) +} + +func TestParseXMLFile(t *testing.T) { + it := ` + # camel-k: pippo=pluto paperino ciao=1 + ### camel-k : ciao + + <?xml version="1.0" encoding="UTF-8"?> + <!-- camel-k: pippo=pluto paperino ciao=1--> + <!--camel-k : ciao --> + <!-- camel-k: language=xml --> + + <routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://camel.apache.org/schema/spring" + xsi:schemaLocation=" + http://camel.apache.org/schema/spring + http://camel.apache.org/schema/spring/camel-spring.xsd"> + + <!-- Write your routes here, for example: --> + <route id="xml"> + <from uri="timer:xml?period=1000"/> + <setBody> + <simple>Hello Camel K from ${routeId}</simple> + </setBody> + <to uri="log:info"/> + </route> + + </routes> + + + ` + opts, err := Parse("example.xml", it) + assert.NoError(t, err) + assert.Len(t, opts, 5) + assert.Contains(t, opts, Option{Name: "pippo", Value: "pluto"}) + assert.Contains(t, opts, Option{Name: "paperino"}) + assert.Contains(t, opts, Option{Name: "ciao", Value: "1"}) + assert.Contains(t, opts, Option{Name: "ciao"}) + assert.Contains(t, opts, Option{Name: "language", Value: "xml"}) +} diff --git a/pkg/util/modeline/types.go b/pkg/util/modeline/types.go new file mode 100644 index 0000000..e7c6840 --- /dev/null +++ b/pkg/util/modeline/types.go @@ -0,0 +1,7 @@ +package modeline + +// Option represents a key/(optional)value modeline option +type Option struct { + Name string + Value string +}