Skip to content

Commit

Permalink
Proper support for fork + for from directory + solution show (#342)
Browse files Browse the repository at this point in the history
* added library for loading and manipulating a solution in memory
* added `--source-dir` flag in `fork`
* support for `solution show`
* support for changing status object types
* added lint for dir/filename matching solution name
* new fork from platform solution
* implemented new, safer unzip using afero
* reset solution version on fork
  • Loading branch information
pnickolov committed Apr 16, 2024
1 parent 0232dc4 commit d8cc22b
Show file tree
Hide file tree
Showing 9 changed files with 1,501 additions and 171 deletions.
70 changes: 57 additions & 13 deletions cmd/solution/download.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023 Cisco Systems, Inc.
// Copyright 2024 Cisco Systems, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -11,10 +11,14 @@
// 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 solution

import (
"errors"
"fmt"
"os"
"path/filepath"

"github.com/apex/log"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -48,28 +52,68 @@ func getSolutionDownloadCmd() *cobra.Command {

func downloadSolution(cmd *cobra.Command, args []string) {
solutionName := getSolutionNameFromArgs(cmd, args, "name")
solutionNameWithZipExtension := getSolutionNameWithZip(solutionName)
solutionTagFlag, _ := cmd.Flags().GetString("tag")

if _, err := DownloadSolutionPackage(solutionName, solutionTagFlag, "."); err != nil {
log.Fatal(err.Error())
}

message := fmt.Sprintf("Solution %q with tag %s downloaded successfully.\n", solutionName, solutionTagFlag)
output.PrintCmdStatus(cmd, message)
}

// DownloadSolutionPackage downloads the solution package into the specified target path
// targetPath may be one of the following:
// - the empty string: download to a temporary file
// - a directory: download to a file in that directory
// - a file: download to that file
// The function returns the path to the downloaded file and error.
func DownloadSolutionPackage(name string, tag string, targetPath string) (string, error) {
// validate name and tag
if !IsValidSolutionName(name) {
return "", fmt.Errorf("invalid solution name %q", name)
}
if !IsValidSolutionTag(tag) {
return "", fmt.Errorf("invalid solution tag %q", tag)
}

// determine the target file path
if targetPath != "" {
// absolutize path
targetPath = absolutizePath(targetPath)

// if targetPath is an existing directory, place zip there; otherwise, treat as file path
fileInfo, err := os.Stat(targetPath)
if err == nil && fileInfo.IsDir() {
targetPath = filepath.Join(filepath.Dir(targetPath), name+".zip")
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
return "", fmt.Errorf("failed to access target path %q: %v", targetPath, err)
} // else treat as file path, possibly overwriting existing file
} else {
// create unique file name in the temporary directory
archive, err := os.CreateTemp("", name+"*.zip") // "*" will be replaced with unique string
if err != nil {
log.Fatalf("failed to create temporary archive file: %v", err)
}
targetPath = archive.Name()
}

headers := map[string]string{
"stage": "STABLE",
"tag": solutionTagFlag,
"solutionFileName": solutionNameWithZipExtension,
"stage": "STABLE", // TODO: check if needed
"tag": tag,
"solutionFileName": targetPath,
}
httpOptions := api.Options{Headers: headers}
bufRes := make([]byte, 0)
if err := api.HTTPGet(getSolutionDownloadUrl(solutionName), &bufRes, &httpOptions); err != nil {
log.Fatalf("Solution download command failed: %v", err)
if err := api.HTTPGet(getSolutionDownloadUrl(name), &bufRes, &httpOptions); err != nil {
return "", fmt.Errorf("Solution download command failed: %v", err)
}

message := fmt.Sprintf("Solution %q with tag %s downloaded successfully.\n", solutionName, solutionTagFlag)
output.PrintCmdStatus(cmd, message)
log.WithFields(log.Fields{"solution": name, "tag": tag, "path": targetPath}).Info("Solution archive downloaded successfully")

return targetPath, nil
}

func getSolutionDownloadUrl(solutionName string) string {
return fmt.Sprintf("solution-manager/v1/solutions/%s", solutionName)
}

func getSolutionNameWithZip(solutionName string) string {
return fmt.Sprintf("%s.zip", solutionName)
}
234 changes: 234 additions & 0 deletions cmd/solution/fork-legacy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// Copyright 2023 Cisco Systems, Inc.
//
// Licensed 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 solution

import (
"archive/zip"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/apex/log"
"github.com/spf13/afero"
"github.com/spf13/afero/zipfs"
"github.com/spf13/cobra"

"github.com/cisco-open/fsoc/output"
"github.com/cisco-open/fsoc/platform/api"
)

// Legacy algorithm to fork a solution using global string replace and assuming json manifest
// Remove this code, together with the `--legacy-replace` flag, once the new solution is proven to work well

func legacyFork(cmd *cobra.Command, solutionName string, solutionTag string, forkName string, fileSystemRoot afero.Fs, fileSystem afero.Fs) {
// download the solution zip file to the current directory
downloadSolutionZip(cmd, solutionName, solutionTag, forkName)

message := fmt.Sprintf("Solution %s was successfully downloaded in the this directory.\r\n", solutionName)
output.PrintCmdStatus(cmd, message)

message = fmt.Sprintf("Changing solution name in manifest to %s.\r\n", forkName)
output.PrintCmdStatus(cmd, message)

// extract files into the newly created solution directory
err := extractZip(fileSystemRoot, fileSystem, solutionName)
if err != nil {
log.Fatalf("Failed to copy files from the zip file to current directory: %v", err)
}

// global replace of solution name in all files in place
editManifest(fileSystem, forkName)

// cleanup the zip file (TODO: move to temp folder and skip cleanup)
err = fileSystemRoot.Remove("./" + solutionName + ".zip")
if err != nil {
log.Fatalf("Failed to remove zip file in current directory: %v", err)
}
}

func editManifest(fileSystem afero.Fs, forkName string) {
manifestFile, err := afero.ReadFile(fileSystem, "./manifest.json")
if err != nil {
log.Fatalf("Error opening manifest file: %v", err)
}

var manifest Manifest
err = json.Unmarshal(manifestFile, &manifest)
if err != nil {
log.Errorf("Failed to parse solution manifest: %v", err)
}

err = refactorSolution(fileSystem, &manifest, forkName)
if err != nil {
log.Errorf("Failed to refactor component definition files within the solution: %v", err)
}
manifest.Name = forkName

f, err := fileSystem.OpenFile("./manifest.json", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
log.Fatalf("Can't open manifest file: %v", err)
}
defer f.Close()
err = output.WriteJson(manifest, f)
if err != nil {
log.Errorf("Failed to to write to solution manifest: %v", err)
}
}

func refactorSolution(fileSystem afero.Fs, manifest *Manifest, forkName string) error {
objDefs := manifest.Objects
var err error
for _, objDef := range objDefs {
if objDef.ObjectsFile != "" {
err = ReplaceStringInFile(fileSystem, objDef.ObjectsFile, manifest.Name, forkName)
} else {
wkDir, _ := os.Getwd()
dirPath := fmt.Sprintf("%s/%s/%s", wkDir, forkName, objDef.ObjectsDir)
err = filepath.Walk(dirPath,
func(path string, info os.FileInfo, err error) error {
// if err != nil {
// return err
// }
if !info.IsDir() {
removeStr := fmt.Sprintf("%s/%s/", wkDir, forkName)
filePath := strings.ReplaceAll(path, removeStr, "")
err = ReplaceStringInFile(fileSystem, filePath, manifest.Name, forkName)
}
return err
})
}
}
return err
}

func ReplaceStringInFile(fileSystem afero.Fs, filePath string, searchValue string, replaceValue string) error {
data, err := afero.ReadFile(fileSystem, filePath)
if err != nil {
return err
}
newFileContent := string(data)
newFileContent = strings.ReplaceAll(newFileContent, searchValue, replaceValue)
err = afero.WriteFile(fileSystem, filePath, []byte(newFileContent), os.FileMode(os.O_RDWR))
if err != nil {
return err
}
return nil
}

func extractZip(rootFileSystem afero.Fs, fileSystem afero.Fs, solutionName string) error {
zipFile, err := rootFileSystem.OpenFile("./"+solutionName+".zip", os.O_RDONLY, os.FileMode(0644))
if err != nil {
log.Fatalf("Error opening zip file: %v", err)
}
fileInfo, err := rootFileSystem.Stat("./" + solutionName + ".zip")
if err != nil {
log.Errorf("Err reading zip: %v", err)
}
reader, _ := zip.NewReader(zipFile, fileInfo.Size())
zipFileSystem := zipfs.New(reader)
dirInfo, _ := afero.ReadDir(zipFileSystem, "./")
err = copyFolderToLocal(zipFileSystem, fileSystem, dirInfo[0].Name())
return err
}

func downloadSolutionZip(cmd *cobra.Command, solutionName string, solutionTag string, forkName string) {
var solutionNameWithZipExtension = solutionName + ".zip"

headers := map[string]string{
"stage": "STABLE", // TODO: check if still needed
"tag": solutionTag,
"solutionFileName": solutionNameWithZipExtension,
}
httpOptions := api.Options{Headers: headers}
bufRes := make([]byte, 0)
if err := api.HTTPGet(getSolutionDownloadUrl(solutionName), &bufRes, &httpOptions); err != nil {
log.Fatalf("Solution download failed: %v", err)
}
}

func ExtractZipToDirectory(archive string, targetFs afero.Fs) error {
archiveFile, err := os.OpenFile(archive, os.O_RDONLY, os.FileMode(0644))
if err != nil {
return fmt.Errorf("error opening zip file: %w", err)
}
defer archiveFile.Close()

archiveFileInfo, err := os.Stat(archive)
if err != nil {
return fmt.Errorf("error determining zip file size: %w", err)
}

reader, _ := zip.NewReader(archiveFile, archiveFileInfo.Size())
zipFileSystem := zipfs.New(reader)
dirInfo, _ := afero.ReadDir(zipFileSystem, "./")
err = copyFolderToLocal(zipFileSystem, targetFs, dirInfo[0].Name())
return err
}

func copyFolderToLocal(zipFileSystem afero.Fs, localFileSystem afero.Fs, subDirectory string) error {
dirInfo, err := afero.ReadDir(zipFileSystem, subDirectory)
if err != nil {
return err
}
for i := range dirInfo {
zipLoc := subDirectory + "/" + dirInfo[i].Name()
localLoc := convertZipLocToLocalLoc(subDirectory + "/" + dirInfo[i].Name())
if !dirInfo[i].IsDir() {
err = copyFile(zipFileSystem, localFileSystem, zipLoc, localLoc)
if err != nil {
return err
}
} else {
err = localFileSystem.Mkdir(localLoc, os.ModeDir)
if err != nil {
return err
}
println(localLoc)
err = localFileSystem.Chmod(localLoc, 0700)
if err != nil {
return err
}
err = copyFolderToLocal(zipFileSystem, localFileSystem, zipLoc)
if err != nil {
return err
}
}
}

return nil
}

func copyFile(zipFileSystem afero.Fs, localFileSystem afero.Fs, zipLoc string, localLoc string) error {
data, err := afero.ReadFile(zipFileSystem, zipLoc)
if err != nil {
return err
}
_, err = localFileSystem.Create(localLoc)
if err != nil {
return err
}
err = afero.WriteFile(localFileSystem, localLoc, data, os.FileMode(os.O_RDWR))
if err != nil {
return err
}
return nil
}

func convertZipLocToLocalLoc(zipLoc string) string {
secondSlashIndex := strings.Index(zipLoc[2:], "/")
return zipLoc[secondSlashIndex+3:]
}

0 comments on commit d8cc22b

Please sign in to comment.