Skip to content

Commit

Permalink
allow URL to be passed in to -f/--file
Browse files Browse the repository at this point in the history
This commit allows to pass URLs besides local files to the
-f/--file switch.

This is done by checking if the passed in resource string starts
with an http:// or https://, and if it does, the URL validation
is done followed by fetching the URL with 3 retry attempts with
a gap of 1 second in between.

Fix #55
  • Loading branch information
concaf committed Apr 4, 2017
1 parent 885542a commit 766cc61
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 2 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ services:
opencompose convert -f hello-nginx.yaml
```

Alternatively, you could also simply pass the URL of the remote file to OpenCompose, like -

```sh
opencompose convert -f https://raw.githubusercontent.com/redhat-developer/opencompose/master/examples/hello-nginx.yaml
```

This will create two Kubernetes files in current directory - `helloworld-deployment.yaml` and `helloworld-service.yaml`.

To deploy your application to Kubernetes run:
Expand Down
15 changes: 13 additions & 2 deletions pkg/cmd/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/redhat-developer/opencompose/pkg/transform"
"github.com/redhat-developer/opencompose/pkg/transform/kubernetes"
"github.com/redhat-developer/opencompose/pkg/transform/openshift"
pkgutil "github.com/redhat-developer/opencompose/pkg/util"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"k8s.io/client-go/pkg/api"
Expand All @@ -28,6 +29,10 @@ var (
opencompose convert -f opencompose.yaml`
)

const (
retryAttempts = 3
)

func NewCmdConvert(v *viper.Viper, out, outerr io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "convert",
Expand Down Expand Up @@ -61,7 +66,6 @@ func GetValidatedObject(v *viper.Viper, cmd *cobra.Command, out, outerr io.Write
if len(files) < 1 {
return nil, cmdutil.UsageError(cmd, "there has to be at least one file")
}

var ocObjects []*object.OpenCompose

for _, file := range files {
Expand All @@ -74,6 +78,13 @@ func GetValidatedObject(v *viper.Viper, cmd *cobra.Command, out, outerr io.Write
if err != nil {
return nil, fmt.Errorf("unable to read from stdin: %s", err)
}
} else if strings.HasPrefix(file, "http://") || strings.HasPrefix(file, "https://") {
// Check if the passed resource is a URL or not
// TODO: add test for validating this against an actual resource on the web
data, err = pkgutil.GetURLData(file, retryAttempts)
if err != nil {
return nil, fmt.Errorf("an error occurred while fetching data from the url %v: %v", file, err)
}
} else {
data, err = ioutil.ReadFile(file)
if err != nil {
Expand All @@ -82,7 +93,7 @@ func GetValidatedObject(v *viper.Viper, cmd *cobra.Command, out, outerr io.Write
}
decoder, err := encoding.GetDecoderFor(data)
if err != nil {
return nil, fmt.Errorf("could not find decoder for file '%s': %s", file, err)
return nil, fmt.Errorf("could not find decoder for resource '%s': %s", file, err)
}

o, err := decoder.Decode(data)
Expand Down
71 changes: 71 additions & 0 deletions pkg/util/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package util

import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
)

// This function validates the URL, then tries to fetch the data with retries
// and then reads and returns the data as []byte
// Returns an error if the URL is invalid, or fetching the data failed or
// if reading the response body fails.
func GetURLData(urlString string, attempts int) ([]byte, error) {
// Validate URL
_, err := url.ParseRequestURI(urlString)
if err != nil {
return nil, fmt.Errorf("invalid URL: %v", urlString)
}

// Fetch the URL and store the response body
data, err := FetchURLWithRetries(urlString, attempts, 1*time.Second)
if err != nil {
return nil, fmt.Errorf("failed fetching data from the URL %v: %s", urlString, err)
}

return data, nil
}

// Try to fetch the given url string, and make <attempts> attempts at it.
// Wait for <duration> time between each try.
// This returns the data from the response body []byte upon successful fetch
// The passed URL is not validated, so validate the URL before passing to this
// function
func FetchURLWithRetries(url string, attempts int, duration time.Duration) ([]byte, error) {
var data []byte
var err error

for i := 0; i < attempts; i++ {
var response *http.Response

// sleep for <duration> seconds before trying again
if i > 0 {
time.Sleep(duration)
}

// retry if http.Get fails
// if all the retries fail, then return statement at the end of the
// function will return this err received from http.Get
response, err = http.Get(url)
if err != nil {
continue
}
defer response.Body.Close()

// if the status code is not 200 OK, return an error
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unable to fetch %v, server returned status code %v", url, response.StatusCode)
}

// Read from the response body, ioutil.ReadAll will return []byte
data, err = ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("reading from response body failed: %s", err)
}
break
}

return data, err
}
65 changes: 65 additions & 0 deletions pkg/util/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package util

import (
"fmt"
"testing"
"time"
)

const (
retryAttempts = 3
)

func TestGetURLData(t *testing.T) {
tests := []struct {
succeed bool
url string
}{
{true, "http://example.com"},
{false, "invalid.url.^&*!@#"},
}

for _, tt := range tests {
t.Run(fmt.Sprintf("passing %v, expected to succeed: %v", tt.url, tt.succeed), func(t *testing.T) {
_, err := GetURLData(tt.url, retryAttempts)

// error occurred but test was expected to pass
if err != nil && tt.succeed {
t.Fatalf("GetURLData was expected to succeed for the URL: %v, but it failed with the error: %v", tt.url, err)
}

// no error occurred but test was expected to fail
if err == nil && !tt.succeed {
t.Fatalf("GetURLData was expected to fail for the URL: %v, but it passed", tt.url)
}
})
}
}

func TestFetchURLWithRetries(t *testing.T) {
tests := []struct {
succeed bool
url string
}{
{true, "https://example.com/"}, // valid URL
{false, "https://invalid.example/"}, // test for no DNS resolution for URL
{false, ""}, // test for blank string
{false, "https://google.com/giveme404"}, // test for !200 status code
}
for _, tt := range tests {
t.Run(fmt.Sprintf("URL %v, expected output: %v", tt.url, tt.succeed), func(t *testing.T) {

_, err := FetchURLWithRetries(tt.url, retryAttempts, time.Second)

// error occurred but tt was expected to pass
if err != nil && tt.succeed {
t.Fatalf("FetchURLWithRetries was expected to succeed for the URL: %v, but it failed with the error: %v", tt.url, err)
}

// no error occurred but test was expected to fail
if err == nil && !tt.succeed {
t.Fatalf("FetchURLWithRetries was expected to fail for the URL: %v, but it passed", tt.url)
}
})
}
}

0 comments on commit 766cc61

Please sign in to comment.