Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding sparseCheckoutDir functionality to devfiles #3042

Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
63bd13a
Initial commit
CameronMcWilliam Apr 28, 2020
85daabe
Merge remote-tracking branch 'upstream/master' into devfileSparseChec…
CameronMcWilliam Apr 28, 2020
2462f56
added devfile
CameronMcWilliam Apr 28, 2020
acf05b0
Merge remote-tracking branch 'upstream/master' into devfileSparseChec…
CameronMcWilliam Apr 29, 2020
4e39e36
added tests
CameronMcWilliam Apr 29, 2020
9a6de10
commented out tests
CameronMcWilliam Apr 29, 2020
67681f5
change to validpath statement
CameronMcWilliam Apr 29, 2020
cfbdd2a
changed method of extracting zip
CameronMcWilliam Apr 30, 2020
b2c8fc8
changed pathsToUnzip to pathToUnzip
CameronMcWilliam Apr 30, 2020
26928c7
Added error message when no files are unzipped
CameronMcWilliam Apr 30, 2020
4968786
cleaned up conditional prefix trim
CameronMcWilliam Apr 30, 2020
dca7532
Changes from feedback
CameronMcWilliam May 1, 2020
330807c
Merge branch 'master' into devfileSparseCheckoutDir
May 11, 2020
ef285fb
feedback changes and unit tests for util func
CameronMcWilliam May 11, 2020
bf0f368
check for empty pathToUnzip
CameronMcWilliam May 12, 2020
4645969
changed error message format
CameronMcWilliam May 12, 2020
8aa566b
cleaned up path separator for windows
CameronMcWilliam May 12, 2020
2c416b8
fixed return pathToUnzip
CameronMcWilliam May 12, 2020
0824130
use hassuffix
CameronMcWilliam May 13, 2020
e109230
moved to function
CameronMcWilliam May 13, 2020
f0b93b0
fromslash
CameronMcWilliam May 13, 2020
080e030
Merge remote-tracking branch 'upstream/master' into devfileSparseChec…
CameronMcWilliam May 20, 2020
14c6c44
minor fixes
CameronMcWilliam May 20, 2020
24e341b
lowercase fix for test
CameronMcWilliam May 20, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions pkg/odo/cli/component/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -766,9 +766,21 @@ func (co *CreateOptions) downloadProject() error {
return errors.Errorf("Project type not supported")
}

err = util.GetAndExtractZip(zipUrl, path)
if err != nil {
return err
if project.Source.SparseCheckoutDir != nil && *project.Source.SparseCheckoutDir != "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code https://github.com/openshift/odo/pull/3042/files#diff-58fe314b4e3f9120402717ab8c2b3049R769-R783 can be extracted into a function, would make things cleaner


sparseCheckoutDir := *project.Source.SparseCheckoutDir
err = util.GetAndExtractZip(zipUrl, path, sparseCheckoutDir)
if err != nil {
return errors.Wrap(err, "failed to download and extract project zip folder")
}

} else {

// extract project to current working directory
err = util.GetAndExtractZip(zipUrl, path, "/")
if err != nil {
return errors.Wrap(err, "failed to download and extract project zip folder")
}
}

return nil
Expand Down
64 changes: 56 additions & 8 deletions pkg/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -801,8 +801,9 @@ func GetGitHubZipURL(repoURL string) (string, error) {
}

// GetAndExtractZip downloads a zip file from a URL with a http prefix or
// takes an absolute path prefixed with file:// and extracts it to a destination
func GetAndExtractZip(zipURL string, destination string) error {
// takes an absolute path prefixed with file:// and extracts it to a destination.
// pathToUnzip specifies the path within the zip folder to extract
func GetAndExtractZip(zipURL string, destination string, pathToUnzip string) error {
if zipURL == "" {
return errors.Errorf("Empty zip url: %s", zipURL)
}
Expand Down Expand Up @@ -836,18 +837,23 @@ func GetAndExtractZip(zipURL string, destination string) error {
return errors.Errorf("Invalid Zip URL: %s . Should either be prefixed with file://, http:// or https://", zipURL)
}

_, err := Unzip(pathToZip, destination)
filenames, err := Unzip(pathToZip, destination, pathToUnzip)
if err != nil {
return err
}

if len(filenames) == 0 {
return errors.New("No files were unzipped, ensure that the project repo is not empty or that sparseCheckoutDir has a valid path")
CameronMcWilliam marked this conversation as resolved.
Show resolved Hide resolved
}

return nil
}

// Unzip will decompress a zip archive, moving all files and folders
// within the zip file (parameter 1) to an output directory (parameter 2).
// Unzip will decompress a zip archive, moving specified files and folders
// within the zip file (parameter 1) to an output directory (parameter 2)
// Source: https://golangcode.com/unzip-files-in-go/
CameronMcWilliam marked this conversation as resolved.
Show resolved Hide resolved
func Unzip(src, dest string) ([]string, error) {
// pathToUnzip (parameter 3) is the path within the zip folder to extract
func Unzip(src, dest, pathToUnzip string) ([]string, error) {
var filenames []string

r, err := zip.OpenReader(src)
Expand All @@ -856,16 +862,48 @@ func Unzip(src, dest string) ([]string, error) {
}
defer r.Close()

for _, f := range r.File {
// change path separator to correct character for windows
if runtime.GOOS == "windows" {
pathToUnzip = strings.Replace(pathToUnzip, "/", string(os.PathSeparator), -1)
CameronMcWilliam marked this conversation as resolved.
Show resolved Hide resolved
}

for _, f := range r.File {
// Store filename/path for returning and using later on
index := strings.Index(f.Name, "/")
index := strings.Index(f.Name, string(os.PathSeparator))
filename := f.Name[index+1:]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could probably use https://golang.org/pkg/path/filepath/#Base here instead of using getting the index of / then breaking the path.

if filename == "" {
continue
}

// if sparseCheckoutDir has a pattern
match, err := filepath.Match(pathToUnzip, filename)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to be careful here with pattern matching:

Using the quarkus devfile, we're pattern matching all of the folders that are prefixed with getting-started, rather than just the one folder getting-started/:

PS D:\ododev\sparse> odo create --downloadSource                                                                        Experimental mode is enabled, use at your own risk

? What do you wish to name the new devfile component mycomp
? What namespace do you want the devfile component to be created in default
 V  Validating devfile component [0ns]

Please use `odo push` command to create the component with source deployed
PS D:\ododev\sparse> ls                                                                                                 

    Directory: D:\ododev\sparse


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       2020-05-06   1:52 PM                -async
d-----       2020-05-06   1:52 PM                -knative
d-----       2020-05-06   1:52 PM                -reactive
d-----       2020-05-06   1:52 PM                -reactive-crud
d-----       2020-05-06   1:52 PM                -testing
d-----       2020-05-06   1:52 PM                .mvn
d-----       2020-05-06   1:52 PM                .odo
d-----       2020-05-06   1:52 PM                .s2i
d-----       2020-05-06   1:52 PM                src
-a----       2020-05-06   1:52 PM             53 .dockerignore
-a----       2020-05-06   1:52 PM            295 .gitignore
-a----       2020-05-06   1:51 PM           2863 devfile.yaml
-a----       2020-05-06   1:52 PM          10078 mvnw
-a----       2020-05-06   1:52 PM           6609 mvnw.cmd
-a----       2020-05-06   1:52 PM           4964 pom.xml
-a----       2020-05-06   1:52 PM           2736 README.md

But when doing it manually via git, it only gives me the getting-started/ folder as expected:

PS D:\ododev\quarkus-quickstarts> git sparse-checkout init --cone                                                       PS D:\ododev\quarkus-quickstarts> git sparse-checkout set getting-started                                               PS D:\ododev\quarkus-quickstarts> ls                                                                                    

    Directory: D:\ododev\quarkus-quickstarts


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       2020-05-06   2:22 PM                getting-started
-a----       2020-05-06   2:21 PM            491 .gitignore
-a----       2020-05-06   2:21 PM           1808 azure-mvn-settings.xml
-a----       2020-05-06   2:21 PM            649 azure-pipelines.yml
-a----       2020-05-06   2:21 PM           2294 CONTRIBUTING.md
-a----       2020-05-06   2:21 PM           6148 gradlew
-a----       2020-05-06   2:21 PM           2942 gradlew.bat
-a----       2020-05-06   2:21 PM          11558 LICENSE
-a----       2020-05-06   2:21 PM          10379 mvnw
-a----       2020-05-06   2:21 PM           6789 mvnw.cmd
-a----       2020-05-06   2:21 PM           3315 pom.xml
-a----       2020-05-06   2:21 PM           6748 README.md

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it seems it was the prefix that was causing this error, I have something in place where we add a path separator to the end of the pathToUnzip if there isn't one present and that seems to fix it in all aspects.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... But I would be willing to hear your opinion on that change :)

if err != nil {
return filenames, err
}

// removes first slash of pathToUnzip if present, adds trailing slash
pathToUnzip = strings.TrimPrefix(pathToUnzip, string(os.PathSeparator))
if pathToUnzip != "" && pathToUnzip[len(pathToUnzip)-1:] != string(os.PathSeparator) {
Copy link
Contributor

@girishramnani girishramnani May 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there has be to be a better way to do this.

pathToUnzip = pathToUnzip + string(os.PathSeparator)
}
// destination filepath before trim
fpath := filepath.Join(dest, filename)

// used for pattern matching
fpathDir := filepath.Dir(fpath)

// check for prefix or match
if strings.HasPrefix(filename, pathToUnzip) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my other note about pattern matching above.

filename = strings.TrimPrefix(filename, pathToUnzip)
} else if !strings.HasPrefix(filename, pathToUnzip) && !match && !sliceContainsString(fpathDir, filenames) {
continue
}
// adds trailing slash to destination if needed as filepath.Join removes it
if (len(filename) == 1 && os.IsPathSeparator(filename[0])) || filename == "" {
fpath = dest + string(os.PathSeparator)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see problems arising in the future in this section with regards to Windows support. Can you double check that your PR works correctly in Windows?

} else {
fpath = filepath.Join(dest, filename)
}
// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
return filenames, fmt.Errorf("%s: illegal file path", fpath)
Expand Down Expand Up @@ -996,3 +1034,13 @@ func ValidateURL(sourceURL string) error {

return nil
}

// sliceContainsString checks for existence of given string in given slice
func sliceContainsString(str string, slice []string) bool {
CameronMcWilliam marked this conversation as resolved.
Show resolved Hide resolved
for _, b := range slice {
if b == str {
return true
}
}
return false
}
38 changes: 38 additions & 0 deletions pkg/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1652,3 +1652,41 @@ func TestValidateURL(t *testing.T) {
})
}
}

func TestSliceContainsString(t *testing.T) {
tests := []struct {
name string
stringVal string
slice []string
wantVal bool
}{
{
name: "Case 1: string in valid slice",
stringVal: "string",
slice: []string{"string", "string2"},
wantVal: true,
},
{
name: "Case 2: string not in valid slice",
stringVal: "string3",
slice: []string{"string", "string2"},
wantVal: false,
},
{
name: "Case 3: string not in empty slice",
stringVal: "string",
slice: []string{},
wantVal: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotVal := sliceContainsString(tt.stringVal, tt.slice)

if !reflect.DeepEqual(gotVal, tt.wantVal) {
t.Errorf("Got %v, want %v", gotVal, tt.wantVal)
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
apiVersion: 1.0.0
metadata:
name: test-devfile
projects:
-
name: nodejs-web-app
source:
type: git
location: "https://github.com/che-samples/web-nodejs-sample.git"
sparseCheckoutDir: /app/
components:
- type: dockerimage
image: quay.io/eclipse/che-nodejs10-ubi:nightly
endpoints:
- name: "3000/tcp"
port: 3000
alias: runtime
env:
- name: FOO
value: "bar"
memoryLimit: 1024Mi
mountSources: true
commands:
- name: build
actions:
- type: exec
component: runtime
command: "npm install"
workdir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app
- name: devbuild
actions:
- type: exec
component: runtime
command: "npm install"
workdir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app
- name: run
actions:
- type: exec
component: runtime
command: "nodemon app.js"
workdir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app
- name: devrun
actions:
- type: exec
component: runtime
command: "nodemon app.js"
workdir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app
32 changes: 32 additions & 0 deletions tests/integration/devfile/cmd_devfile_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,36 @@ var _ = Describe("odo devfile create command tests", func() {
// helper.DeleteDir(contextDevfile)
// })
//})

// Context("When executing odo create with devfile component, --downloadSource flag and sparseContextDir has a valid value", func() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Thanks for writing the tests even if they can't be enabled just yet.

A couple small modifications I'd recommend:

  • Add the --devfile flag to the odo create commands, this way we're not relying on interactive mode (and thus don't need to wait for the integration tests to support it). Then we only need to wait for "--devfile" support for odo create #2862 to be delivered.

// It("should only extract the specified path in the sparseContextDir field", func() {
// helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true")
// contextDevfile := helper.CreateNewContext()
// helper.Chdir(contextDevfile)
// devfile := "devfile.yaml"
// helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile-with-sparseCheckoutDir"), filepath.Join(contextDevfile, devfile))
// componentNamespace := helper.RandString(6)
// helper.CmdShouldPass("odo", "create", "--downloadSource", "--project", componentNamespace)
// expectedFiles := []string{"app.js", devfile}
// Expect(helper.VerifyFilesExist(contextDevfile, expectedFiles)).To(Equal(true))
// helper.DeleteDir(contextDevfile)
// })
// })

// Context("When executing odo create with devfile component, --downloadSource flag and sparseContextDir has an invalid value", func() {
// It("should fail and alert the user that the specified path in sparseContextDir does not exist", func() {
// helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true")
// contextDevfile := helper.CreateNewContext()
// helper.Chdir(contextDevfile)
// devfile := "devfile.yaml"
// devfilePath := filepath.Join(contextDevfile, devfile)
// helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile-with-sparseCheckoutDir"), devfilePath)
// helper.ReplaceDevfileField(devfilePath, "sparseCheckoutDir", "/invalid/")
// componentNamespace := helper.RandString(6)
// output := helper.CmdShouldFail("odo", "create", "--downloadSource", "--project", componentNamespace)
// expectedString := "No files were unzipped, ensure that the project repo is not empty or that sparseCheckoutDir has a valid path"
// helper.MatchAllInOutput(output, []string{expectedString})
// helper.DeleteDir(contextDevfile)
// })
// })
})