This is an automated email from the ASF dual-hosted git repository. lburgazzoli pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/camel-k.git
The following commit(s) were added to refs/heads/master by this push: new b3ae436 Add suport for gists #1740 b3ae436 is described below commit b3ae43691dea0afa6eb05a9a46c9d486f13656e4 Author: Luca Burgazzoli <lburgazz...@gmail.com> AuthorDate: Mon Oct 5 15:53:54 2020 +0200 Add suport for gists #1740 --- .../ROOT/pages/running/run-from-github.adoc | 57 +++++- e2e/common/run_test.go | 64 +++++-- go.mod | 2 + go.sum | 4 + pkg/cmd/modeline.go | 106 +++++------ pkg/cmd/modeline_test.go | 8 +- pkg/cmd/run.go | 96 ++-------- pkg/cmd/util.go | 13 ++ pkg/cmd/{util_getter.go => util_content.go} | 75 +++++--- pkg/cmd/util_sources.go | 201 +++++++++++++++++++++ pkg/util/util.go | 12 ++ 11 files changed, 448 insertions(+), 190 deletions(-) diff --git a/docs/modules/ROOT/pages/running/run-from-github.adoc b/docs/modules/ROOT/pages/running/run-from-github.adoc index 5f071b6..1d606b8 100644 --- a/docs/modules/ROOT/pages/running/run-from-github.adoc +++ b/docs/modules/ROOT/pages/running/run-from-github.adoc @@ -1,25 +1,64 @@ [[run-from-github]] = Run from GitHub -It is possible to run integrations from GitHub with a dedicated URL -syntax: +It is possible to run integrations from a GitHub repository or Gist with dedicated URL syntax: -``` +== Repository + +.Syntax +[source] +---- kamel run github:$user/$repo/$path?branch=$branch -``` +---- As example, running the following command -``` + +[source] +---- kamel run github:apache/camel-k/examples/Sample.java -``` +---- is equivalent to: -``` +[source] +---- kamel run https://raw.githubusercontent.com/apache/camel-k/master/examples/Sample.java -``` +---- but does not require to type the full GitHub RAW URL. -Declaring the branch query param is not required and defaults to `master` if not explicit set. \ No newline at end of file +Declaring the branch query param is not required and defaults to `master` if not explicit set. + +== Gist + +.Syntax +[source] +---- +kamel run https://gist.github.com/${user-id}/${gist-id} +kamel run gist:${gist-id} +---- + +camel-k will add any file that is part of the Gist as a source. + +As example, assuming there are two files listed as part of a Gist, beans.yaml and routes.yaml, then the following command + + +[source] +---- +kamel run gist:${gist-id} +---- + +is equivalent to: + +[source] +---- +kamel run \ + https://gist.githubusercontent.com/${user-id}/${gist-id}/raw/${...}/beans.yaml \ + https://gist.githubusercontent.com/${user-id}/${gist-id}/raw/${...}/routes.yaml +---- + +[NOTE] +==== +GitHub applies rate limiting to its APIs and as Authenticated requests get a higher rate limit, the camel-k cli honour the env var GITHUB_TOKEN and if it is found, then it is used for GitHub authentication. +==== \ No newline at end of file diff --git a/e2e/common/run_test.go b/e2e/common/run_test.go index 1309bf8..3cb833d 100644 --- a/e2e/common/run_test.go +++ b/e2e/common/run_test.go @@ -44,24 +44,6 @@ func TestRunSimpleExamples(t *testing.T) { Expect(Kamel("delete", "--all", "-n", ns).Execute()).Should(BeNil()) }) - t.Run("run java from GitHub", func(t *testing.T) { - RegisterTestingT(t) - Expect(Kamel("run", "-n", ns, "github:apache/camel-k/e2e/common/files/Java.java").Execute()).Should(BeNil()) - Eventually(IntegrationPodPhase(ns, "java"), TestTimeoutMedium).Should(Equal(v1.PodRunning)) - Eventually(IntegrationCondition(ns, "java", camelv1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(v1.ConditionTrue)) - Eventually(IntegrationLogs(ns, "java"), TestTimeoutShort).Should(ContainSubstring("Magicstring!")) - Expect(Kamel("delete", "--all", "-n", ns).Execute()).Should(BeNil()) - }) - - t.Run("run java from GitHub (RAW)", func(t *testing.T) { - RegisterTestingT(t) - Expect(Kamel("run", "-n", ns, "https://raw.githubusercontent.com/apache/camel-k/master/e2e/common/files/Java.java").Execute()).Should(BeNil()) - Eventually(IntegrationPodPhase(ns, "java"), TestTimeoutMedium).Should(Equal(v1.PodRunning)) - Eventually(IntegrationCondition(ns, "java", camelv1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(v1.ConditionTrue)) - Eventually(IntegrationLogs(ns, "java"), TestTimeoutShort).Should(ContainSubstring("Magicstring!")) - Expect(Kamel("delete", "--all", "-n", ns).Execute()).Should(BeNil()) - }) - t.Run("run java with properties", func(t *testing.T) { RegisterTestingT(t) Expect(Kamel("run", "-n", ns, "files/Prop.java", "--property-file", "files/prop.properties").Execute()).Should(BeNil()) @@ -138,3 +120,49 @@ func TestRunSimpleExamples(t *testing.T) { }) } + +func TestRunExamplesFromGitHUB(t *testing.T) { + WithNewTestNamespace(t, func(ns string) { + Expect(Kamel("install", "-n", ns).Execute()).Should(BeNil()) + + t.Run("run java from GitHub", func(t *testing.T) { + RegisterTestingT(t) + Expect(Kamel("run", "-n", ns, "github:apache/camel-k/e2e/common/files/Java.java").Execute()).Should(BeNil()) + Eventually(IntegrationPodPhase(ns, "java"), TestTimeoutMedium).Should(Equal(v1.PodRunning)) + Eventually(IntegrationCondition(ns, "java", camelv1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(v1.ConditionTrue)) + Eventually(IntegrationLogs(ns, "java"), TestTimeoutShort).Should(ContainSubstring("Magicstring!")) + Expect(Kamel("delete", "--all", "-n", ns).Execute()).Should(BeNil()) + }) + + t.Run("run java from GitHub (RAW)", func(t *testing.T) { + RegisterTestingT(t) + Expect(Kamel("run", "-n", ns, "https://raw.githubusercontent.com/apache/camel-k/master/e2e/common/files/Java.java").Execute()).Should(BeNil()) + Eventually(IntegrationPodPhase(ns, "java"), TestTimeoutMedium).Should(Equal(v1.PodRunning)) + Eventually(IntegrationCondition(ns, "java", camelv1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(v1.ConditionTrue)) + Eventually(IntegrationLogs(ns, "java"), TestTimeoutShort).Should(ContainSubstring("Magicstring!")) + Expect(Kamel("delete", "--all", "-n", ns).Execute()).Should(BeNil()) + }) + + t.Run("run from GitHub Gist (ID)", func(t *testing.T) { + name := "github-gist-id" + RegisterTestingT(t) + Expect(Kamel("run", "-n", ns, "--name", name, "gist:e2c3f9a5fd0d9e79b21b04809786f17a").Execute()).Should(BeNil()) + Eventually(IntegrationPodPhase(ns, name), TestTimeoutMedium).Should(Equal(v1.PodRunning)) + Eventually(IntegrationCondition(ns, name, camelv1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(v1.ConditionTrue)) + Eventually(IntegrationLogs(ns, name), TestTimeoutShort).Should(ContainSubstring("Magicstring!")) + Eventually(IntegrationLogs(ns, name), TestTimeoutShort).Should(ContainSubstring("Tick!")) + Expect(Kamel("delete", "--all", "-n", ns).Execute()).Should(BeNil()) + }) + + t.Run("run from GitHub Gist (URL)", func(t *testing.T) { + name := "github-gist-url" + RegisterTestingT(t) + Expect(Kamel("run", "-n", ns, "--name", name, "https://gist.github.com/lburgazzoli/e2c3f9a5fd0d9e79b21b04809786f17a").Execute()).Should(BeNil()) + Eventually(IntegrationPodPhase(ns, name), TestTimeoutMedium).Should(Equal(v1.PodRunning)) + Eventually(IntegrationCondition(ns, name, camelv1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(v1.ConditionTrue)) + Eventually(IntegrationLogs(ns, name), TestTimeoutShort).Should(ContainSubstring("Magicstring!")) + Eventually(IntegrationLogs(ns, name), TestTimeoutShort).Should(ContainSubstring("Tick!")) + Expect(Kamel("delete", "--all", "-n", ns).Execute()).Should(BeNil()) + }) + }) +} diff --git a/go.mod b/go.mod index b0fe8b6..2a218d4 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/fatih/structs v1.1.0 github.com/gertd/go-pluralize v0.1.1 github.com/go-logr/logr v0.1.0 + github.com/google/go-github/v32 v32.1.0 github.com/google/uuid v1.1.1 github.com/jpillora/backoff v1.0.0 github.com/magiconair/properties v1.8.1 @@ -35,6 +36,7 @@ require ( github.com/stoewer/go-strcase v1.0.2 github.com/stretchr/testify v1.5.1 go.uber.org/multierr v1.5.0 + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d gopkg.in/inf.v0 v0.9.1 gopkg.in/yaml.v2 v2.3.0 k8s.io/api v0.18.9 diff --git a/go.sum b/go.sum index 7d03bdb..20d9cc2 100644 --- a/go.sum +++ b/go.sum @@ -585,11 +585,15 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-github/v27 v27.0.6/go.mod h1:/0Gr8pJ55COkmv+S/yPKCczSkUPIM/LnFyubufRNIS0= github.com/google/go-github/v29 v29.0.3/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E= +github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II= +github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= github.com/google/go-licenses v0.0.0-20191112164736-212ea350c932/go.mod h1:16wa6pRqNDUIhOtwF0GcROVqMeXHZJ7H6eGDFUh5Pfk= github.com/google/go-licenses v0.0.0-20200227160636-0fa8c766a591/go.mod h1:JWeTIGPLQ9gF618ZOdlUitd1gRR/l99WOkHOlmR/UVA= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= diff --git a/pkg/cmd/modeline.go b/pkg/cmd/modeline.go index 1e1ca37..4eab57a 100644 --- a/pkg/cmd/modeline.go +++ b/pkg/cmd/modeline.go @@ -47,7 +47,6 @@ var ( // 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, @@ -55,10 +54,10 @@ var ( } ) +// NewKamelWithModelineCommand --- func NewKamelWithModelineCommand(ctx context.Context, osArgs []string) (*cobra.Command, []string, error) { - processed := make(map[string]bool) originalFlags := osArgs[1:] - rootCmd, flags, err := createKamelWithModelineCommand(ctx, append([]string(nil), originalFlags...), processed) + rootCmd, flags, err := createKamelWithModelineCommand(ctx, originalFlags) if err != nil { fmt.Printf("Error: %s\n", err.Error()) return rootCmd, flags, err @@ -75,7 +74,7 @@ func NewKamelWithModelineCommand(ctx context.Context, osArgs []string) (*cobra.C return rootCmd, flags, nil } -func createKamelWithModelineCommand(ctx context.Context, args []string, processedFiles map[string]bool) (*cobra.Command, []string, error) { +func createKamelWithModelineCommand(ctx context.Context, args []string) (*cobra.Command, []string, error) { rootCmd, err := NewKamelCommand(ctx) if err != nil { return nil, nil, err @@ -99,44 +98,20 @@ func createKamelWithModelineCommand(ctx context.Context, args []string, processe fg := target.Flags() - sources, err := fg.GetStringArray(runCmdSourcesArgs) + additionalSources, err := fg.GetStringArray(runCmdSourcesArgs) if err != nil { return nil, nil, err } - var files = append([]string(nil), fg.Args()...) - files = append(files, sources...) + files := make([]string, 0, len(fg.Args())+len(additionalSources)) + files = append(files, fg.Args()...) + files = append(files, additionalSources...) - opts := make([]modeline.Option, 0) - for _, f := range files { - if processedFiles[f] { - continue - } - baseDir := filepath.Dir(f) - content, _, err := loadData(f, false, 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 disallowedOptions[o.Name] { - return nil, nil, fmt.Errorf("option %q is disallowed in modeline", o.Name) - } - - if fileOptions[o.Name] && isLocal(f) { - refPath := o.Value - if !filepath.IsAbs(refPath) { - full := path.Join(baseDir, refPath) - o.Value = full - ops[i] = o - } - } - } - opts = append(opts, ops...) + opts, err := extractModelineOptions(ctx, files) + if err != nil { + return nil, nil, errors.Wrap(err, "cannot read sources") } + // filter out in place non-run options nOpts := 0 for _, o := range opts { @@ -145,23 +120,9 @@ func createKamelWithModelineCommand(ctx context.Context, args []string, processe 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 - } + opts = opts[:nOpts] - // New options added, recomputing - for _, f := range files { - processedFiles[f] = true - } for _, o := range opts { prefix := "-" if len(o.Name) > 1 { @@ -175,5 +136,46 @@ func createKamelWithModelineCommand(ctx context.Context, args []string, processe } } - return createKamelWithModelineCommand(ctx, args, processedFiles) + // 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 +} + +func extractModelineOptions(ctx context.Context, sources []string) ([]modeline.Option, error) { + opts := make([]modeline.Option, 0) + + resolvedSources, err := ResolveSources(ctx, sources, false) + if err != nil { + return opts, errors.Wrap(err, "cannot read sources") + } + + for _, resolvedSource := range resolvedSources { + ops, err := modeline.Parse(resolvedSource.Location, resolvedSource.Content) + if err != nil { + return opts, errors.Wrapf(err, "cannot process file %s", resolvedSource.Location) + } + for i, o := range ops { + if disallowedOptions[o.Name] { + return opts, fmt.Errorf("option %q is disallowed in modeline", o.Name) + } + + if fileOptions[o.Name] && resolvedSource.Local { + baseDir := filepath.Dir(resolvedSource.Origin) + refPath := o.Value + if !filepath.IsAbs(refPath) { + full := path.Join(baseDir, refPath) + o.Value = full + ops[i] = o + } + } + } + opts = append(opts, ops...) + } + + return opts, nil } diff --git a/pkg/cmd/modeline_test.go b/pkg/cmd/modeline_test.go index 20e91bd..ca6037a 100644 --- a/pkg/cmd/modeline_test.go +++ b/pkg/cmd/modeline_test.go @@ -82,23 +82,23 @@ func TestModelineRunMultipleFiles(t *testing.T) { defer os.RemoveAll(dir) file := ` - // camel-k: source=ext.groovy + // camel-k: dependency=mvn:org.my/lib1:3.0 ` 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:3.0 + // camel-k: dependency=mvn:org.my/lib2:3.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}) + cmd, flags, err := NewKamelWithModelineCommand(context.TODO(), []string{"kamel", "run", fileName, fileName2}) assert.NoError(t, err) assert.NotNil(t, cmd) - assert.Equal(t, []string{"run", fileName, "--source=" + fileName2, "--dependency=mvn:org.my/lib:3.0"}, flags) + assert.Equal(t, []string{"run", fileName, fileName2, "--dependency=mvn:org.my/lib1:3.0", "--dependency=mvn:org.my/lib2:3.0"}, flags) } func TestModelineRunPropertyFiles(t *testing.T) { diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index db2c7c3..c515e77 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -18,13 +18,10 @@ limitations under the License. package cmd import ( - "bytes" - "encoding/base64" + "context" "encoding/json" "fmt" "io/ioutil" - "net/http" - "net/url" "os" "os/signal" "path" @@ -49,7 +46,6 @@ import ( "github.com/apache/camel-k/pkg/trait" "github.com/apache/camel-k/pkg/util" "github.com/apache/camel-k/pkg/util/flow" - "github.com/apache/camel-k/pkg/util/gzip" "github.com/apache/camel-k/pkg/util/kubernetes" k8slog "github.com/apache/camel-k/pkg/util/kubernetes/log" "github.com/apache/camel-k/pkg/util/sync" @@ -207,19 +203,8 @@ func (o *runCmdOptions) validateArgs(_ *cobra.Command, args []string) error { return errors.New("run expects at least 1 argument, received 0") } - for _, source := range args { - if isLocal(source) { - if _, err := os.Stat(source); err != nil && os.IsNotExist(err) { - return errors.Wrapf(err, "file %s does not exist", source) - } else if err != nil { - return errors.Wrapf(err, "error while accessing file %s", source) - } - } else { - _, _, err := loadData(source, false, false) - if err != nil { - return errors.Wrap(err, "The provided source is not reachable") - } - } + if _, err := ResolveSources(context.Background(), args, false); err != nil { + return errors.Wrap(err, "One of the provided sources is not reachable") } return nil @@ -414,7 +399,7 @@ func (o *runCmdOptions) syncIntegration(cmd *cobra.Command, c client.Client, sou return case <-changes: // let's create a new command to parse modeline changes and update our integration - newCmd, _, err := createKamelWithModelineCommand(o.RootContext, os.Args[1:], make(map[string]bool)) + newCmd, _, err := createKamelWithModelineCommand(o.RootContext, os.Args[1:]) newCmd.SetOut(cmd.OutOrStdout()) newCmd.SetErr(cmd.ErrOrStderr()) if err != nil { @@ -493,14 +478,14 @@ func (o *runCmdOptions) updateIntegrationCode(c client.Client, sources []string, srcs = append(srcs, sources...) srcs = append(srcs, o.Sources...) - for _, source := range srcs { - data, compressed, err := loadData(source, o.Compression, o.CompressBinary) - if err != nil { - return nil, err - } + resolvedSources, err := ResolveSources(context.Background(), srcs, o.Compression) + if err != nil { + return nil, err + } - if !compressed && o.UseFlows && (strings.HasSuffix(source, ".yaml") || strings.HasSuffix(source, ".yml")) { - flows, err := flow.FromYamlDSLString(data) + for _, source := range resolvedSources { + if o.UseFlows && !o.Compression && (strings.HasSuffix(source.Name, ".yaml") || strings.HasSuffix(source.Name, ".yml")) { + flows, err := flow.FromYamlDSLString(source.Content) if err != nil { return nil, err } @@ -508,16 +493,16 @@ func (o *runCmdOptions) updateIntegrationCode(c client.Client, sources []string, } else { integration.Spec.AddSources(v1.SourceSpec{ DataSpec: v1.DataSpec{ - Name: path.Base(source), - Content: data, - Compression: compressed, + Name: source.Name, + Content: source.Content, + Compression: source.Compress, }, }) } } for _, resource := range o.Resources { - data, compressed, err := loadData(resource, o.Compression, o.CompressBinary) + data, compressed, err := loadContent(resource, o.Compression, o.CompressBinary) if err != nil { return nil, err } @@ -533,7 +518,7 @@ func (o *runCmdOptions) updateIntegrationCode(c client.Client, sources []string, } for _, resource := range o.OpenAPIs { - data, compressed, err := loadData(resource, o.Compression, o.CompressBinary) + data, compressed, err := loadContent(resource, o.Compression, o.CompressBinary) if err != nil { return nil, err } @@ -603,7 +588,7 @@ func (o *runCmdOptions) updateIntegrationCode(c client.Client, sources []string, } existed := false - err := c.Create(o.Context, &integration) + err = c.Create(o.Context, &integration) if err != nil && k8serrors.IsAlreadyExists(err) { existed = true clone := integration.DeepCopy() @@ -656,53 +641,6 @@ func (o *runCmdOptions) GetIntegrationName(sources []string) string { return name } -func loadData(source string, compress bool, compressBinary bool) (string, bool, error) { - var content []byte - var err error - - if isLocal(source) { - content, err = ioutil.ReadFile(source) - if err != nil { - return "", false, err - } - } else { - u, err := url.Parse(source) - if err != nil { - return "", false, err - } - - g, ok := Getters[u.Scheme] - if !ok { - return "", false, fmt.Errorf("unable to find a getter for URL: %s", source) - } - - content, err = g.Get(u) - if err != nil { - return "", false, err - } - } - - doCompress := compress - if !doCompress && compressBinary { - contentType := http.DetectContentType(content) - if strings.HasPrefix(contentType, "application/octet-stream") { - doCompress = true - } - } - - if doCompress { - var b bytes.Buffer - - if err := gzip.Compress(&b, content); err != nil { - return "", false, err - } - - return base64.StdEncoding.EncodeToString(b.Bytes()), true, nil - } - - return string(content), false, nil -} - func (*runCmdOptions) configureTraits(integration *v1.Integration, options []string, catalog *trait.Catalog) error { traits, err := configureTraits(options, catalog) if err != nil { diff --git a/pkg/cmd/util.go b/pkg/cmd/util.go index 6cfff5f..8d5ac14 100644 --- a/pkg/cmd/util.go +++ b/pkg/cmd/util.go @@ -18,10 +18,13 @@ limitations under the License. package cmd import ( + "bytes" "context" + "encoding/base64" "encoding/csv" "encoding/json" "fmt" + "github.com/apache/camel-k/pkg/util/gzip" "log" "reflect" "strings" @@ -237,3 +240,13 @@ func fieldByMapstructureTagName(target reflect.Value, tagName string) (reflect.S return reflect.StructField{}, false } + +func compressToString(content []byte) (string, error) { + var b bytes.Buffer + + if err := gzip.Compress(&b, content); err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString(b.Bytes()), nil +} diff --git a/pkg/cmd/util_getter.go b/pkg/cmd/util_content.go similarity index 56% rename from pkg/cmd/util_getter.go rename to pkg/cmd/util_content.go index 4be7526..75d9c08 100644 --- a/pkg/cmd/util_getter.go +++ b/pkg/cmd/util_content.go @@ -23,34 +23,55 @@ import ( "net/http" "net/url" "regexp" + "strings" ) -var Getters map[string]Getter - -func init() { - Getters = map[string]Getter{ - "http": HTTPGetter{}, - "https": HTTPGetter{}, - "github": GitHubGetter{}, +func loadContent(source string, compress bool, compressBinary bool) (string, bool, error) { + var content []byte + var err error + + if isLocal(source) { + content, err = ioutil.ReadFile(source) + } else { + u, err := url.Parse(source) + if err != nil { + return "", false, err + } + + switch u.Scheme { + case "github": + content, err = loadContentGitHub(u) + case "http": + content, err = loadContentHTTP(u) + case "https": + content, err = loadContentHTTP(u) + default: + return "", false, fmt.Errorf("unsupported scheme %s", u.Scheme) + } } -} -type Getter interface { - Get(u *url.URL) ([]byte, error) -} + if err != nil { + return "", false, err + } + doCompress := compress + if !doCompress && compressBinary { + contentType := http.DetectContentType(content) + if strings.HasPrefix(contentType, "application/octet-stream") { + doCompress = true + } + } -// A simple getter that retrieves the content of an integration from an -// http(s) endpoint. -type HTTPGetter struct { -} + if doCompress { + answer, err := compressToString(content) + return answer, true, err + } -func (g HTTPGetter) Get(u *url.URL) ([]byte, error) { - return g.doGet(u.String()) + return string(content), false, nil } -func (g HTTPGetter) doGet(source string) ([]byte, error) { +func loadContentHTTP(u *url.URL) ([]byte, error) { // nolint: gosec - resp, err := http.Get(source) + resp, err := http.Get(u.String()) if err != nil { return []byte{}, err } @@ -59,7 +80,7 @@ func (g HTTPGetter) doGet(source string) ([]byte, error) { }() if resp.StatusCode != 200 { - return []byte{}, fmt.Errorf("the provided URL %s is not reachable, error code is %d", source, resp.StatusCode) + return []byte{}, fmt.Errorf("the provided URL %s is not reachable, error code is %d", u.String(), resp.StatusCode) } content, err := ioutil.ReadAll(resp.Body) @@ -70,13 +91,7 @@ func (g HTTPGetter) doGet(source string) ([]byte, error) { return content, nil } -// A simple getter that retrieves the content of an integration from -// a GitHub endpoint using a RAW endpoint. -type GitHubGetter struct { - HTTPGetter -} - -func (g GitHubGetter) Get(u *url.URL) ([]byte, error) { +func loadContentGitHub(u *url.URL) ([]byte, error) { src := u.Scheme + ":" + u.Opaque re := regexp.MustCompile(`^github:([^/]+)/([^/]+)/(.+)$`) @@ -91,6 +106,10 @@ func (g GitHubGetter) Get(u *url.URL) ([]byte, error) { } srcURL := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s", items[1], items[2], branch, items[3]) + rawURL, err := url.Parse(srcURL) + if err != nil { + return []byte{}, err + } - return g.HTTPGetter.doGet(srcURL) + return loadContentHTTP(rawURL) } diff --git a/pkg/cmd/util_sources.go b/pkg/cmd/util_sources.go new file mode 100644 index 0000000..af218f0 --- /dev/null +++ b/pkg/cmd/util_sources.go @@ -0,0 +1,201 @@ +/* +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" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "path" + "strings" + + "github.com/apache/camel-k/pkg/util" + + "golang.org/x/oauth2" + + "github.com/google/go-github/v32/github" + "github.com/pkg/errors" +) + +// Source --- +type Source struct { + Origin string + Location string + Name string + Content string + Compress bool + Local bool +} + +func (s *Source) setContent(content []byte) error { + if s.Compress { + result, err := compressToString(content) + if err != nil { + return err + } + + s.Content = result + } else { + s.Content = string(content) + } + + return nil +} + +// ResolveSources --- +func ResolveSources(ctx context.Context, locations []string, compress bool) ([]Source, error) { + sources := make([]Source, 0, len(locations)) + + for _, location := range locations { + if isLocal(location) { + if _, err := os.Stat(location); err != nil && os.IsNotExist(err) { + return sources, errors.Wrapf(err, "file %s does not exist", location) + } else if err != nil { + return sources, errors.Wrapf(err, "error while accessing file %s", location) + } + + answer := Source{ + Name: path.Base(location), + Origin: location, + Location: location, + Compress: compress, + Local: true, + } + + content, err := ioutil.ReadFile(location) + if err != nil { + return sources, err + } + if err := answer.setContent(content); err != nil { + return sources, err + } + + sources = append(sources, answer) + } else { + u, err := url.Parse(location) + if err != nil { + return sources, err + } + + switch { + case u.Scheme == "gist" || strings.HasPrefix(location, "https://gist.github.com/"): + var tc *http.Client + + if token, ok := os.LookupEnv("GITHUB_TOKEN"); ok { + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + tc = oauth2.NewClient(ctx, ts) + + fmt.Println("GITHUB_TOKEN env var detected, using it for GitHub APIs authentication") + } + + gc := github.NewClient(tc) + gistID := "" + + if strings.HasPrefix(location, "https://gist.github.com/") { + names := util.FindNamedMatches(`^https://gist.github.com/(([a-zA-Z0-9]*)/)?(?P<gistid>[a-zA-Z0-9]*)$`, location) + if value, ok := names["gistid"]; ok { + gistID = value + } + } else { + gistID = u.Opaque + } + + if gistID == "" { + return sources, fmt.Errorf("unable to determing gist id from %s", location) + } + + gists, _, err := gc.Gists.Get(ctx, gistID) + if err != nil { + return sources, err + } + + for _, v := range gists.Files { + if v.Filename == nil || v.Content == nil { + continue + } + + answer := Source{ + Name: *v.Filename, + Compress: compress, + Origin: location, + } + if v.RawURL != nil { + answer.Location = *v.RawURL + } + if err := answer.setContent([]byte(*v.Content)); err != nil { + return sources, err + } + sources = append(sources, answer) + } + case u.Scheme == "github": + answer := Source{ + Name: path.Base(location), + Origin: location, + Location: location, + Compress: compress, + } + + content, err := loadContentGitHub(u) + if err != nil { + return sources, err + } + if err := answer.setContent(content); err != nil { + return sources, err + } + sources = append(sources, answer) + case u.Scheme == "http": + answer := Source{ + Name: path.Base(location), + Origin: location, + Location: location, + Compress: compress, + } + + content, err := loadContentHTTP(u) + if err != nil { + return sources, err + } + if err := answer.setContent(content); err != nil { + return sources, err + } + sources = append(sources, answer) + case u.Scheme == "https": + answer := Source{ + Name: path.Base(location), + Origin: location, + Location: location, + Compress: compress, + } + + content, err := loadContentHTTP(u) + if err != nil { + return sources, err + } + if err := answer.setContent(content); err != nil { + return sources, err + } + sources = append(sources, answer) + } + } + } + + return sources, nil +} diff --git a/pkg/util/util.go b/pkg/util/util.go index cfd7e18..1867126 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -209,6 +209,18 @@ func FindAllDistinctStringSubmatch(data string, regexps ...*regexp.Regexp) []str return submatchs.List() } +// FindNamedMatches --- +func FindNamedMatches(expr string, str string) map[string]string { + regex := regexp.MustCompile(expr) + match := regex.FindStringSubmatch(str) + + results := map[string]string{} + for i, name := range match { + results[regex.SubexpNames()[i]] = name + } + return results +} + // FileExists -- func FileExists(name string) (bool, error) { info, err := os.Stat(name)