diff --git a/taskfile/node.go b/taskfile/node.go index 486a0a16f0..02e03d7e44 100644 --- a/taskfile/node.go +++ b/taskfile/node.go @@ -21,7 +21,7 @@ type Node interface { Remote() bool ResolveEntrypoint(entrypoint string) (string, error) ResolveDir(dir string) (string, error) - FilenameAndLastDir() (string, string) + FilenameAndLastDir() (lastDir string, file string) // TODO the return order is implemented opposite to the naming } func NewRootNode( diff --git a/taskfile/node_git.go b/taskfile/node_git.go index 557986d541..644c562da6 100644 --- a/taskfile/node_git.go +++ b/taskfile/node_git.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "net/url" "path/filepath" "strings" @@ -22,10 +21,10 @@ import ( // An GitNode is a node that reads a Taskfile from a remote location via Git. type GitNode struct { *BaseNode - URL *url.URL - rawUrl string - ref string - path string + fullURL string + baseURL string + ref string + filepath string } func NewGitNode( @@ -34,37 +33,37 @@ func NewGitNode( insecure bool, opts ...NodeOption, ) (*GitNode, error) { - base := NewBaseNode(dir, opts...) - u, err := giturls.Parse(entrypoint) + gitURL, err := giturls.Parse(entrypoint) if err != nil { return nil, err } + if gitURL.Scheme == "http" && !insecure { + return nil, &errors.TaskfileNotSecureError{URI: entrypoint} + } - basePath, path := func() (string, string) { - x := strings.Split(u.Path, "//") + urlPath, filepath := func() (string, string) { + x := strings.Split(gitURL.Path, "//") return x[0], x[1] }() - ref := u.Query().Get("ref") - rawUrl := u.String() + ref := gitURL.Query().Get("ref") + fullURL := gitURL.Redacted() - u.RawQuery = "" - u.Path = basePath + gitURL.RawQuery = "" + gitURL.Path = urlPath + baseURL := gitURL.String() - if u.Scheme == "http" && !insecure { - return nil, &errors.TaskfileNotSecureError{URI: entrypoint} - } return &GitNode{ - BaseNode: base, - URL: u, - rawUrl: rawUrl, + BaseNode: NewBaseNode(dir, opts...), + fullURL: fullURL, + baseURL: baseURL, ref: ref, - path: path, + filepath: filepath, }, nil } func (node *GitNode) Location() string { - return node.rawUrl + return node.fullURL } func (node *GitNode) Remote() bool { @@ -75,7 +74,7 @@ func (node *GitNode) Read(_ context.Context) ([]byte, error) { fs := memfs.New() storer := memory.NewStorage() _, err := git.Clone(storer, fs, &git.CloneOptions{ - URL: node.URL.String(), + URL: node.baseURL, ReferenceName: plumbing.ReferenceName(node.ref), SingleBranch: true, Depth: 1, @@ -83,7 +82,7 @@ func (node *GitNode) Read(_ context.Context) ([]byte, error) { if err != nil { return nil, err } - file, err := fs.Open(node.path) + file, err := fs.Open(node.filepath) if err != nil { return nil, err } @@ -97,8 +96,8 @@ func (node *GitNode) Read(_ context.Context) ([]byte, error) { } func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) { - dir, _ := filepath.Split(node.path) - resolvedEntrypoint := fmt.Sprintf("%s//%s", node.URL, filepath.Join(dir, entrypoint)) + dir, _ := filepath.Split(node.filepath) + resolvedEntrypoint := fmt.Sprintf("%s//%s", node.baseURL, filepath.Join(dir, entrypoint)) if node.ref != "" { return fmt.Sprintf("%s?ref=%s", resolvedEntrypoint, node.ref), nil } @@ -122,5 +121,5 @@ func (node *GitNode) ResolveDir(dir string) (string, error) { } func (node *GitNode) FilenameAndLastDir() (string, string) { - return filepath.Base(node.path), filepath.Base(filepath.Dir(node.path)) + return filepath.Base(filepath.Dir(node.filepath)), filepath.Base(node.filepath) } diff --git a/taskfile/node_git_test.go b/taskfile/node_git_test.go index 2df39c54ba..cd5715ab31 100644 --- a/taskfile/node_git_test.go +++ b/taskfile/node_git_test.go @@ -12,9 +12,9 @@ func TestGitNode_ssh(t *testing.T) { node, err := NewGitNode("git@github.com:foo/bar.git//Taskfile.yml?ref=main", "", false) assert.NoError(t, err) assert.Equal(t, "main", node.ref) - assert.Equal(t, "Taskfile.yml", node.path) - assert.Equal(t, "ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl) - assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String()) + assert.Equal(t, "Taskfile.yml", node.filepath) + assert.Equal(t, "ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main", node.fullURL) + assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.baseURL) entrypoint, err := node.ResolveEntrypoint("common.yml") assert.NoError(t, err) assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint) @@ -26,9 +26,9 @@ func TestGitNode_sshWithDir(t *testing.T) { node, err := NewGitNode("git@github.com:foo/bar.git//directory/Taskfile.yml?ref=main", "", false) assert.NoError(t, err) assert.Equal(t, "main", node.ref) - assert.Equal(t, "directory/Taskfile.yml", node.path) - assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl) - assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String()) + assert.Equal(t, "directory/Taskfile.yml", node.filepath) + assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.fullURL) + assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.baseURL) entrypoint, err := node.ResolveEntrypoint("common.yml") assert.NoError(t, err) assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint) @@ -37,29 +37,29 @@ func TestGitNode_sshWithDir(t *testing.T) { func TestGitNode_https(t *testing.T) { t.Parallel() - node, err := NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false) + node, err := NewGitNode("https://git:token@github.com/foo/bar.git//Taskfile.yml?ref=main", "", false) assert.NoError(t, err) assert.Equal(t, "main", node.ref) - assert.Equal(t, "Taskfile.yml", node.path) - assert.Equal(t, "https://github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl) - assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String()) + assert.Equal(t, "Taskfile.yml", node.filepath) + assert.Equal(t, "https://git:xxxxx@github.com/foo/bar.git//Taskfile.yml?ref=main", node.fullURL) + assert.Equal(t, "https://git:token@github.com/foo/bar.git", node.baseURL) entrypoint, err := node.ResolveEntrypoint("common.yml") assert.NoError(t, err) - assert.Equal(t, "https://github.com/foo/bar.git//common.yml?ref=main", entrypoint) + assert.Equal(t, "https://git:token@github.com/foo/bar.git//common.yml?ref=main", entrypoint) } func TestGitNode_httpsWithDir(t *testing.T) { t.Parallel() - node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false) + node, err := NewGitNode("https://git:token@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false) assert.NoError(t, err) assert.Equal(t, "main", node.ref) - assert.Equal(t, "directory/Taskfile.yml", node.path) - assert.Equal(t, "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl) - assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String()) + assert.Equal(t, "directory/Taskfile.yml", node.filepath) + assert.Equal(t, "https://git:xxxxx@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.fullURL) + assert.Equal(t, "https://git:token@github.com/foo/bar.git", node.baseURL) entrypoint, err := node.ResolveEntrypoint("common.yml") assert.NoError(t, err) - assert.Equal(t, "https://github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint) + assert.Equal(t, "https://git:token@github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint) } func TestGitNode_FilenameAndDir(t *testing.T) { @@ -67,19 +67,19 @@ func TestGitNode_FilenameAndDir(t *testing.T) { node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false) assert.NoError(t, err) - filename, dir := node.FilenameAndLastDir() + dir, filename := node.FilenameAndLastDir() assert.Equal(t, "Taskfile.yml", filename) assert.Equal(t, "directory", dir) node, err = NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false) assert.NoError(t, err) - filename, dir = node.FilenameAndLastDir() + dir, filename = node.FilenameAndLastDir() assert.Equal(t, "Taskfile.yml", filename) assert.Equal(t, ".", dir) node, err = NewGitNode("https://github.com/foo/bar.git//multiple/directory/Taskfile.yml?ref=main", "", false) assert.NoError(t, err) - filename, dir = node.FilenameAndLastDir() + dir, filename = node.FilenameAndLastDir() assert.Equal(t, "Taskfile.yml", filename) assert.Equal(t, "directory", dir) } diff --git a/taskfile/node_http.go b/taskfile/node_http.go index 6e1529722e..497f6f0cfc 100644 --- a/taskfile/node_http.go +++ b/taskfile/node_http.go @@ -16,7 +16,7 @@ import ( // An HTTPNode is a node that reads a Taskfile from a remote location via HTTP. type HTTPNode struct { *BaseNode - URL *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml) + url *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml) entrypoint string // stores entrypoint url. used for building graph vertices. timeout time.Duration } @@ -34,13 +34,13 @@ func NewHTTPNode( return nil, err } if url.Scheme == "http" && !insecure { - return nil, &errors.TaskfileNotSecureError{URI: entrypoint} + return nil, &errors.TaskfileNotSecureError{URI: url.Redacted()} } return &HTTPNode{ BaseNode: base, - URL: url, - entrypoint: entrypoint, + url: url, + entrypoint: url.Redacted(), timeout: timeout, }, nil } @@ -54,27 +54,27 @@ func (node *HTTPNode) Remote() bool { } func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) { - url, err := RemoteExists(ctx, node.URL, node.timeout) + url, err := RemoteExists(ctx, node.url, node.timeout) if err != nil { return nil, err } - node.URL = url - req, err := http.NewRequest("GET", node.URL.String(), nil) + node.url = url + req, err := http.NewRequest("GET", node.url.String(), nil) if err != nil { - return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()} + return nil, errors.TaskfileFetchFailedError{URI: node.url.Redacted()} } resp, err := http.DefaultClient.Do(req.WithContext(ctx)) if err != nil { if errors.Is(err, context.DeadlineExceeded) { - return nil, &errors.TaskfileNetworkTimeoutError{URI: node.URL.String(), Timeout: node.timeout} + return nil, &errors.TaskfileNetworkTimeoutError{URI: node.url.Redacted(), Timeout: node.timeout} } - return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()} + return nil, errors.TaskfileFetchFailedError{URI: node.url.Redacted()} } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.TaskfileFetchFailedError{ - URI: node.URL.String(), + URI: node.url.Redacted(), HTTPStatusCode: resp.StatusCode, } } @@ -93,7 +93,7 @@ func (node *HTTPNode) ResolveEntrypoint(entrypoint string) (string, error) { if err != nil { return "", err } - return node.URL.ResolveReference(ref).String(), nil + return node.url.ResolveReference(ref).String(), nil } func (node *HTTPNode) ResolveDir(dir string) (string, error) { @@ -118,5 +118,9 @@ func (node *HTTPNode) ResolveDir(dir string) (string, error) { func (node *HTTPNode) FilenameAndLastDir() (string, string) { dir, filename := filepath.Split(node.entrypoint) + dir = filepath.Base(dir) + if dir == node.url.Host { + dir = "." + } return filepath.Base(dir), filename } diff --git a/taskfile/node_http_test.go b/taskfile/node_http_test.go new file mode 100644 index 0000000000..dc6c36500c --- /dev/null +++ b/taskfile/node_http_test.go @@ -0,0 +1,76 @@ +package taskfile + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHTTPNode_https(t *testing.T) { + t.Parallel() + + node, err := NewHTTPNode("https://raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml", "", false, time.Second) + require.NoError(t, err) + assert.Equal(t, time.Second, node.timeout) + assert.Equal(t, "https://raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml", node.url.String()) + entrypoint, err := node.ResolveEntrypoint("common.yml") + require.NoError(t, err) + assert.Equal(t, "https://raw.githubusercontent.com/my-org/my-repo/main/common.yml", entrypoint) +} + +func TestHTTPNode_redaction(t *testing.T) { + t.Parallel() + + node, err := NewHTTPNode("https://user:password@example.com/Taskfile.yml", "", false, time.Second) + + t.Run("the location is redacted", func(t *testing.T) { + t.Parallel() + require.NoError(t, err) + assert.Equal(t, "https://user:xxxxx@example.com/Taskfile.yml", node.Location()) + }) + + t.Run("resolved entrypoints contain the username and password", func(t *testing.T) { + t.Parallel() + location, err := node.ResolveEntrypoint("common.yaml") + require.NoError(t, err) + assert.Equal(t, "https://user:password@example.com/common.yaml", location) + }) +} + +func TestHTTPNode_FilenameAndDir(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + entrypoint string + filename string + dir string + }{ + "file at root": { + entrypoint: "https://example.com/Taskfile.yaml", + filename: "Taskfile.yaml", + dir: ".", + }, + "file in folder": { + entrypoint: "https://example.com/taskfiles/Taskfile.yaml", + filename: "Taskfile.yaml", + dir: "taskfiles", + }, + "nested structure": { + entrypoint: "https://raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yaml", + filename: "Taskfile.yaml", + dir: "main", + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + node, err := NewHTTPNode(tt.entrypoint, "", false, time.Second) + require.NoError(t, err) + dir, filename := node.FilenameAndLastDir() + assert.Equal(t, tt.filename, filename) + assert.Equal(t, tt.dir, dir) + }) + } +}