Skip to content

Commit

Permalink
Add go-getter support to SDK. Add support for watching files specifie…
Browse files Browse the repository at this point in the history
…d by connection config properties with the tag watch. Closes #451. Closes #434
  • Loading branch information
kaidaguerre authored Nov 16, 2022
1 parent 99a4108 commit ef68f9c
Show file tree
Hide file tree
Showing 31 changed files with 1,984 additions and 656 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/acceptance-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Run SDK Unit Tests
run: |
go clean -testcache
go test -timeout 30s ./...
go test -timeout 600s ./...
buildChaosPlugin:
Expand Down
216 changes: 216 additions & 0 deletions getter/get_files.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package getter

import (
"fmt"
"net/url"
"os"
"path"
"strings"
"time"

"github.com/hashicorp/go-getter"
filehelpers "github.com/turbot/go-kit/files"
)

// GetFiles determines whether sourcePath is a local or remote path
// - if local, it returns the root directory and any glob specified
// - if remote, it uses go-getter to download the files into a temporary location,
// then returns the location and the glob used the retrieve the files
//
// and the glob
func GetFiles(sourcePath, tmpDir string) (localSourcePath string, globPattern string, err error) {
if sourcePath == "" {
return "", "", fmt.Errorf("source cannot be empty")
}

// check whether sourcePath is a glob with a root location which exists in the file system
localSourcePath, globPattern, err = filehelpers.GlobRoot(sourcePath)
if err != nil {
return "", "", err
}
// if we managed to resolve the sourceDir, treat this as a local path
if localSourcePath != "" {
return localSourcePath, globPattern, nil
}

remoteSourcePath, globPattern, urlData, err := resolveGlobAndSourcePath(sourcePath)
if err != nil {
return "", "", err
}

// create temporary directory to store the go-getter data
dest := createTempDirForGet(tmpDir)

// If there is no glob pattern, source path is a filename - make the glob pattern the full DESTINATION file path
if globPattern == "" {

// if the source path is a S3 URL, and the path refers to a top-level file, for example:
// s3::https://bucket.s3.amazonaws.com/foo.ext
// send the path directly to go-getter, and use the destination path as glob pattern for file searching
// and also remove the query parameters (parts after ?) from destination path

parts := strings.Split(remoteSourcePath, string(os.PathSeparator))
// extract file name from remoteSourcePath
filename := parts[len(parts)-1]
// build the dest filename and assign to glob
dest = path.Join(dest, filename)
globPattern = dest
} else {
// so this is a folder - apply special case s3 handling
remoteSourcePath, dest = handleS3FolderPath(remoteSourcePath, dest)
}

// is there was a query string, escape the values and add to the url
remoteSourcePath = addQueryToSourcePath(urlData, remoteSourcePath)

err = getter.Get(dest, remoteSourcePath)
if err != nil {
return "", "", fmt.Errorf("failed to get directory specified by the source %s: %s", remoteSourcePath, err.Error())
}

if globPattern != "" && dest != globPattern {
globPattern = path.Join(dest, globPattern)
}

return dest, globPattern, nil
}

func addQueryToSourcePath(urlData *url.URL, sourcePath string) string {
// if any query string passed in the URL, it will appear in u.RawQuery
// (in other words we have stripped out the glob)
if urlData.RawQuery == "" {
return sourcePath
}

// iterate through all the query params and escape the characters (if needed)
values := urlData.Query()
for k := range values {
// we must use values.Get rather that ranging over k,v as the value is an array
// and Get returns the first value
values.Set(k, url.QueryEscape(values.Get(k)))
}
queryString := values.Encode()

// append the query params to the source path
return fmt.Sprintf("%s?%s", sourcePath, queryString)
}

func handleS3FolderPath(remoteSourcePath string, dest string) (string, string) {
// When querying s3 with go-getter, we specify the folder as part of the url
// (as opposed to after a double slash - as it is for github/gitlab etc)
// e.g:
// s3::https://my-bucket.s3.us-east-1.amazonaws.com/test_folder//*.ext?aws_profile=test_profile
//
// In this case we need to extract the folder path from the url to build the dest path.
//
// We do this by taking everything after the string "amazonaws.com/"
if strings.Contains(remoteSourcePath, "amazonaws.com") {
// add the bucket folder to the end of the dest path
sourceSplit := strings.Split(remoteSourcePath, "amazonaws.com/")
if len(sourceSplit) > 1 {
// extract the folder path, (removing query params, which will be re-added later)
folderName := strings.Split(sourceSplit[1], "?")[0]
// set dest to the dest folder path
dest = path.Join(dest, folderName)
}

// go-getter supports an extra / at the end of the source path
// which will download all data stored in that path

// for example if source path is specified as:
// s3::https://my-bucket.s3.us-east-1.amazonaws.com//*.ext?aws_profile=test_profile
// we need to pass the following to go getter
// (query param is re-added later)
// s3::https://my-bucket.s3.us-east-1.amazonaws.com/?aws_profile=test_profile
if !strings.HasSuffix(remoteSourcePath, "/") {
remoteSourcePath += "/"
}
}
return remoteSourcePath, dest
}

func resolveGlobAndSourcePath(sourcePath string) (remoteSourcePath, glob string, urlData *url.URL, err error) {
// parse source path to extract the raw query string
u, err := url.Parse(sourcePath)
if err != nil {

return "", "", nil, fmt.Errorf("failed to parse the source %s: %s", sourcePath, err.Error())
}

// rebuild the source path without any query params
remoteSourcePath = removeQueryParams(u, remoteSourcePath)

// extract the glob
// e.g. for github.com/turbot/steampipe-plugin-alicloud//*.tf"
// remoteSourcePath is github.com/turbot/steampipe-plugin-alicloud
// glob is *.tf
remoteSourcePath, globPattern := extractGlob(remoteSourcePath)

// // if the source path for S3 has a '/' at the end, go-getter downloads all the contents stored inside that bucket.
// // For example:
// // s3::https://bucket.s3.us-east-1.amazonaws.com/
// if strings.HasSuffix(sourcePath, ".amazonaws.com") {
// sourcePath = fmt.Sprintf("%s/", sourcePath)
// }

return remoteSourcePath, globPattern, u, nil
}

func removeQueryParams(u *url.URL, remoteSourcePath string) string {

if u.Scheme == "" {
// no scheme specified
// e.g. gitlab.com/subhajit7/example-files//terraform-examples//*.tf
remoteSourcePath = u.Path
} else {
// go getter supports s3 and git urls which have prefixes s3:: or git::
// For example:
// s3::bucket.s3.amazonaws.com/test//*.tf?aws_profile=check&region=us-east-1
// git::bitbucket.org/benturrell/terraform-arcgis-portal
// In these cases host and path comes empty while parsing the URL
if u.Host != "" && u.Path != "" {
// i.e. https, http
remoteSourcePath = fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Path)
} else {
// i.e. s3::, git::
remoteSourcePath = fmt.Sprintf("%s:%s", u.Scheme, u.Opaque)
}
}
return remoteSourcePath
}

// extract the glob pattern from the source path
func extractGlob(remoteSourcePath string) (string, string) {
var globPattern string
lastIndex := strings.LastIndex(remoteSourcePath, "//")
// if this is NOT the '//' after a http://
if lastIndex != -1 && remoteSourcePath[lastIndex-1:lastIndex] != ":" {
globPattern = remoteSourcePath[lastIndex+2:]
remoteSourcePath = remoteSourcePath[:lastIndex]
}
return remoteSourcePath, globPattern
}

// create a uniquely named sub-directory
func createTempDirForGet(tmpDir string) string {
var dest string
for {
dest = path.Join(tmpDir, timestamp())
_, err := os.Stat(dest)
if err == nil {
break
}

// return true if unique
if os.IsNotExist(err) {
break
}
}

return dest
}

// get the current timestamp
func timestamp() string {
return time.Now().UTC().Format(time.RFC3339)
}
62 changes: 62 additions & 0 deletions getter/get_files_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package getter

import (
"net/url"
"testing"
)

type getFilesTest struct {
FullSourcePath string
RemoteSourcePath string
Expected string
}

var getFilesTestCases = map[string]getFilesTest{
"no special characters": {
FullSourcePath: "s3.amazonaws.com/bucket///*.tf?aws_access_key_id=ABCDEFGHIJ4KLMNO3PQ&aws_access_key_secret=ThisIsateSt123secr3tT0Val1dat3",
RemoteSourcePath: "s3.amazonaws.com/bucket/",
Expected: "s3.amazonaws.com/bucket/?aws_access_key_id=ABCDEFGHIJ4KLMNO3PQ&aws_access_key_secret=ThisIsateSt123secr3tT0Val1dat3",
},
"access key - special characters - +": {
FullSourcePath: "s3.amazonaws.com/bucket///*.tf?aws_access_key_id=ABCDEFGH+J4KLMNO3PQ&aws_access_key_secret=ThisIsateSt123secr3tT0Val1dat3",
RemoteSourcePath: "s3.amazonaws.com/bucket/",
Expected: "s3.amazonaws.com/bucket/?aws_access_key_id=ABCDEFGH%2BJ4KLMNO3PQ&aws_access_key_secret=ThisIsateSt123secr3tT0Val1dat3",
},
"secret key - special characters - +": {
FullSourcePath: "s3.amazonaws.com/bucket///*.tf?aws_access_key_id=ABCDEFGHIJ4KLMNO3PQ&aws_access_key_secret=ThisIsateSt12+secr3tT0Val1dat3",
RemoteSourcePath: "s3.amazonaws.com/bucket/",
Expected: "s3.amazonaws.com/bucket/?aws_access_key_id=ABCDEFGHIJ4KLMNO3PQ&aws_access_key_secret=ThisIsateSt12%2Bsecr3tT0Val1dat3",
},
"access key - special characters - /": {
FullSourcePath: "s3.amazonaws.com/bucket///*.tf?aws_access_key_id=ABCDEFGH/J4KLMNO3PQ&aws_access_key_secret=ThisIsateSt123secr3tT0Val1dat3",
RemoteSourcePath: "s3.amazonaws.com/bucket/",
Expected: "s3.amazonaws.com/bucket/?aws_access_key_id=ABCDEFGH%252FJ4KLMNO3PQ&aws_access_key_secret=ThisIsateSt123secr3tT0Val1dat3",
},
"secret key - special characters - //": {
FullSourcePath: "s3.amazonaws.com/bucket///*.tf?aws_access_key_id=ABCDEFGHIJ4KLMNO3PQ&aws_access_key_secret=ThisIsateSt1//secr3tT0Val1dat3",
RemoteSourcePath: "s3.amazonaws.com/bucket/",
Expected: "s3.amazonaws.com/bucket/?aws_access_key_id=ABCDEFGHIJ4KLMNO3PQ&aws_access_key_secret=ThisIsateSt1%252F%252Fsecr3tT0Val1dat3",
},
"session token - special characters": {
FullSourcePath: "s3.amazonaws.com/bucket///*.tf?aws_access_key_id=ABCDEFGHIJ4KLMNO3PQ&aws_access_key_secret=ThisIsateSt1//secr3tT0Val1dat3&aws_access_token=lsghglhshgjsgjejgrekg74592645982jlsdhjlgfhs////fsjfhjwlohfowyhhfhwq6796264629hwkhfyy69+ljgdj",
RemoteSourcePath: "s3.amazonaws.com/bucket/",
Expected: "s3.amazonaws.com/bucket/?aws_access_key_id=ABCDEFGHIJ4KLMNO3PQ&aws_access_key_secret=ThisIsateSt1%252F%252Fsecr3tT0Val1dat3&aws_access_token=lsghglhshgjsgjejgrekg74592645982jlsdhjlgfhs%252F%252F%252F%252Ffsjfhjwlohfowyhhfhwq6796264629hwkhfyy69%2Bljgdj",
},
}

func TestGetFiles(t *testing.T) {
for name, test := range getFilesTestCases {

// parse source path to extract the raw query string
u, err := url.Parse(test.FullSourcePath)
if err != nil {
t.Errorf(`failed to parse the sourcePath: %s`, test.FullSourcePath)
}

remoteSourcePath := addQueryToSourcePath(u, test.RemoteSourcePath)

if remoteSourcePath != test.Expected {
t.Errorf(`Test: '%s'' FAILED : expected %v, got %v`, name, test.Expected, remoteSourcePath)
}
}
}
33 changes: 28 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/olekukonko/tablewriter v0.0.5
github.com/sethvargo/go-retry v0.1.0
github.com/stevenle/topsort v0.0.0-20130922064739-8130c1d7596b
github.com/turbot/go-kit v0.4.0
github.com/turbot/go-kit v0.5.0-rc.4
github.com/zclconf/go-cty v1.12.1
go.opentelemetry.io/otel v1.10.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.30.0
Expand All @@ -34,31 +34,46 @@ require (
require (
github.com/allegro/bigcache/v3 v3.0.2
github.com/eko/gocache/v3 v3.1.1
github.com/fsnotify/fsnotify v1.5.4
github.com/hashicorp/go-getter v1.6.2
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
)

require (
cloud.google.com/go v0.65.0 // indirect
cloud.google.com/go/storage v1.10.0 // indirect
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/agext/levenshtein v1.2.2 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/aws/aws-sdk-go v1.15.78 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
github.com/btubbs/datetime v0.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 // indirect
github.com/jstemmer/go-junit-report v0.9.1 // indirect
github.com/klauspost/compress v1.11.2 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/go-homedir v1.0.0 // indirect
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
Expand All @@ -73,12 +88,20 @@ require (
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/tkrajina/go-reflector v0.5.4 // indirect
github.com/ulikunitz/xz v0.5.8 // indirect
go.opencensus.io v0.22.4 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0 // indirect
go.opentelemetry.io/proto/otlp v0.16.0 // indirect
golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf // indirect
golang.org/x/net v0.0.0-20220412020605-290c469a71a5 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/exp v0.0.0-20221109205753-fc8884afc316 // indirect
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
golang.org/x/mod v0.6.0 // indirect
golang.org/x/net v0.1.0 // indirect
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/tools v0.2.0 // indirect
google.golang.org/api v0.30.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
Expand Down
Loading

0 comments on commit ef68f9c

Please sign in to comment.