From edfaf2b5b0bc450fbb68eee36b6fbad1102411d4 Mon Sep 17 00:00:00 2001 From: "Badr, Nesma" Date: Mon, 9 Sep 2024 16:04:51 +0200 Subject: [PATCH] Parse Manifest and Default CR --- cmd/modulectl/cmd.go | 4 +- internal/service/create/create.go | 12 +++ .../reader/moduleconfig_reader.go | 63 ++++++++++++++- .../reader/moduleconfig_reader_test.go | 13 ++++ tools/filesystem/tempfilesystem.go | 76 +++++++++++++++++++ 5 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 tools/filesystem/tempfilesystem.go diff --git a/cmd/modulectl/cmd.go b/cmd/modulectl/cmd.go index 8b823cc0..a447ca88 100644 --- a/cmd/modulectl/cmd.go +++ b/cmd/modulectl/cmd.go @@ -71,8 +71,10 @@ func NewCmd() (*cobra.Command, error) { func buildModuleService() (*create.Service, error) { fileSystemUtil := &filesystem.Util{} + tmpFileSystem := filesystem.NewTempFileSystem() + defer tmpFileSystem.RemoveTempFiles() - moduleConfigService, err := moduleconfigreader.NewService(fileSystemUtil) + moduleConfigService, err := moduleconfigreader.NewService(fileSystemUtil, tmpFileSystem) if err != nil { return nil, fmt.Errorf("failed to create module config service: %w", err) } diff --git a/internal/service/create/create.go b/internal/service/create/create.go index 6884fc72..947949e3 100644 --- a/internal/service/create/create.go +++ b/internal/service/create/create.go @@ -10,6 +10,8 @@ import ( type ModuleConfigService interface { ParseModuleConfig(configFilePath string) (*contentprovider.ModuleConfig, error) ValidateModuleConfig(moduleConfig *contentprovider.ModuleConfig) error + GetDefaultCRPath(defaultCRPath string) (string, error) + GetManifestPath(manifestPath string) (string, error) } type Service struct { @@ -40,5 +42,15 @@ func (s *Service) CreateModule(opts Options) error { return fmt.Errorf("%w: failed to value module config", err) } + moduleConfig.DefaultCRPath, err = s.moduleConfigService.GetDefaultCRPath(moduleConfig.DefaultCRPath) + if err != nil { + return fmt.Errorf("%w: failed to get default CR path", err) + } + + moduleConfig.ManifestPath, err = s.moduleConfigService.GetManifestPath(moduleConfig.ManifestPath) + if err != nil { + return fmt.Errorf("%w: failed to get manifest path", err) + } + return nil } diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader.go b/internal/service/moduleconfig/reader/moduleconfig_reader.go index ea30c35b..d9f3dd68 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader.go @@ -2,6 +2,7 @@ package moduleconfigreader import ( "fmt" + "net/url" "gopkg.in/yaml.v3" @@ -10,21 +11,37 @@ import ( "github.com/kyma-project/modulectl/internal/service/contentprovider" ) +const ( + defaultCRFilePattern = "kyma-module-default-cr-*.yaml" + defaultManifestFilePattern = "kyma-module-manifest-*.yaml" +) + type FileSystem interface { ReadFile(path string) ([]byte, error) } +type TempFileSystem interface { + DownloadTempFile(dir, pattern string, url *url.URL) (string, error) + RemoveTempFiles() []error +} + type Service struct { - fileSystem FileSystem + fileSystem FileSystem + tmpFileSystem TempFileSystem } -func NewService(fileSystem FileSystem) (*Service, error) { +func NewService(fileSystem FileSystem, tmpFileSystem TempFileSystem) (*Service, error) { if fileSystem == nil { return nil, fmt.Errorf("%w: fileSystem must not be nil", commonerrors.ErrInvalidArg) } + if tmpFileSystem == nil { + return nil, fmt.Errorf("%w: tmpFileSystem must not be nil", commonerrors.ErrInvalidArg) + } + return &Service{ - fileSystem: fileSystem, + fileSystem: fileSystem, + tmpFileSystem: tmpFileSystem, }, nil } @@ -42,6 +59,42 @@ func (s *Service) ParseModuleConfig(configFilePath string) (*contentprovider.Mod return moduleConfig, nil } +func (s *Service) GetDefaultCRPath(defaultCRPath string) (string, error) { + if defaultCRPath == "" { + return defaultCRPath, nil + } + + path := defaultCRPath + if parsedURL, err := s.ParseURL(defaultCRPath); err == nil { + path, err = s.tmpFileSystem.DownloadTempFile("", defaultCRFilePattern, parsedURL) + if err != nil { + return "", fmt.Errorf("failed to download default CR file: %w", err) + } + } + + return path, nil +} + +func (s *Service) GetManifestPath(manifestPath string) (string, error) { + path := manifestPath + if parsedURL, err := s.ParseURL(manifestPath); err == nil { + path, err = s.tmpFileSystem.DownloadTempFile("", defaultManifestFilePattern, parsedURL) + if err != nil { + return "", fmt.Errorf("failed to download default CR file: %w", err) + } + } + + return path, nil +} + +func (s *Service) ParseURL(urlString string) (*url.URL, error) { + urlParsed, err := url.Parse(urlString) + if err != nil && urlParsed.Scheme != "" && urlParsed.Host != "" { + return urlParsed, nil + } + return nil, fmt.Errorf("%w: parsing url failed for %s", commonerrors.ErrInvalidArg, urlString) +} + func (*Service) ValidateModuleConfig(moduleConfig *contentprovider.ModuleConfig) error { if err := validations.ValidateModuleName(moduleConfig.Name); err != nil { return fmt.Errorf("failed to validate module name: %w", err) @@ -59,5 +112,9 @@ func (*Service) ValidateModuleConfig(moduleConfig *contentprovider.ModuleConfig) return fmt.Errorf("failed to validate module namespace: %w", err) } + if moduleConfig.ManifestPath == "" { + return fmt.Errorf("%w: manifest path must not be empty", commonerrors.ErrInvalidArg) + } + return nil } diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go index ceb91568..eba53faa 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go @@ -2,6 +2,7 @@ package moduleconfigreader_test import ( "errors" + "net/url" "testing" "github.com/stretchr/testify/assert" @@ -19,6 +20,7 @@ const ( func Test_ParseModuleConfig_ReturnsError_WhenFileReaderReturnsError(t *testing.T) { svc, _ := moduleconfigreader.NewService( &fileDoesNotExistStub{}, + &tmpfileSystemStub{}, ) result, err := svc.ParseModuleConfig(moduleConfigFile) @@ -30,6 +32,7 @@ func Test_ParseModuleConfig_ReturnsError_WhenFileReaderReturnsError(t *testing.T func Test_ParseModuleConfig_ReturnsCorrect_ModuleConfig(t *testing.T) { svc, _ := moduleconfigreader.NewService( &fileExistsStub{}, + &tmpfileSystemStub{}, ) result, err := svc.ParseModuleConfig(moduleConfigFile) @@ -78,6 +81,16 @@ func (*fileExistsStub) ReadFile(_ string) ([]byte, error) { return yaml.Marshal(moduleConfig) } +type tmpfileSystemStub struct{} + +func (*tmpfileSystemStub) DownloadTempFile(_ string, _ string, _ *url.URL) (string, error) { + return "test", nil +} + +func (*tmpfileSystemStub) RemoveTempFiles() []error { + return nil +} + type fileDoesNotExistStub struct{} func (*fileDoesNotExistStub) FileExists(_ string) (bool, error) { diff --git a/tools/filesystem/tempfilesystem.go b/tools/filesystem/tempfilesystem.go new file mode 100644 index 00000000..1cb4686a --- /dev/null +++ b/tools/filesystem/tempfilesystem.go @@ -0,0 +1,76 @@ +package filesystem + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "time" +) + +const httpGetTimeout = 20 * time.Second + +var errBadHTTPStatus = errors.New("bad http status") + +type TempFileSystem struct { + files []*os.File +} + +func NewTempFileSystem() *TempFileSystem { + return &TempFileSystem{files: []*os.File{}} +} + +func (fs *TempFileSystem) DownloadTempFile(dir, pattern string, url *url.URL) (string, error) { + bytes, err := getBytesFromURL(url) + if err != nil { + return "", fmt.Errorf("failed to download file from %s: %w", url, err) + } + + tmpFile, err := os.CreateTemp(dir, pattern) + if err != nil { + return "", fmt.Errorf("failed to create temp file with pattern %s: %w", pattern, err) + } + defer tmpFile.Close() + fs.files = append(fs.files, tmpFile) + if _, err := tmpFile.Write(bytes); err != nil { + return "", fmt.Errorf("failed to write to temp file %s: %w", tmpFile.Name(), err) + } + return tmpFile.Name(), nil +} + +func (fs *TempFileSystem) RemoveTempFiles() []error { + var errs []error + for _, file := range fs.files { + err := os.Remove(file.Name()) + if err != nil { + errs = append(errs, err) + } + } + fs.files = []*os.File{} + return errs +} + +func getBytesFromURL(url *url.URL) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), httpGetTimeout) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil) + if err != nil { + return nil, fmt.Errorf("http GET request failed for %s: %w", url, err) + } + defer req.Body.Close() + + if req.Response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%w: bad status for GET request to %s: %q", errBadHTTPStatus, url, + req.Response.StatusCode) + } + + data, err := io.ReadAll(req.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body from %s: %w", url, err) + } + + return data, nil +}