diff --git a/.golangci.yaml b/.golangci.yaml index ca37a337..ff9ec118 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -17,9 +17,6 @@ linters: - paralleltest # should be enabled consciously for long running tests - wsl # too strict and mostly code is not more readable linters-settings: - gomoddirectives: - replace-allow-list: - - ocm.software/ocm revive: severity: error rules: diff --git a/cmd/modulectl/cmd.go b/cmd/modulectl/cmd.go index 75cce5cd..c90e8582 100644 --- a/cmd/modulectl/cmd.go +++ b/cmd/modulectl/cmd.go @@ -17,6 +17,7 @@ import ( "github.com/kyma-project/modulectl/internal/service/create" "github.com/kyma-project/modulectl/internal/service/filegenerator" "github.com/kyma-project/modulectl/internal/service/filegenerator/reusefilegenerator" + "github.com/kyma-project/modulectl/internal/service/fileresolver" "github.com/kyma-project/modulectl/internal/service/git" moduleconfiggenerator "github.com/kyma-project/modulectl/internal/service/moduleconfig/generator" moduleconfigreader "github.com/kyma-project/modulectl/internal/service/moduleconfig/reader" @@ -89,7 +90,17 @@ func buildModuleService() (*create.Service, error) { fileSystemUtil := &filesystem.Util{} tmpFileSystem := filesystem.NewTempFileSystem() - moduleConfigService, err := moduleconfigreader.NewService(fileSystemUtil, tmpFileSystem) + manifestFileResolver, err := fileresolver.NewFileResolver("kyma-module-manifest-*.yaml", tmpFileSystem) + if err != nil { + return nil, fmt.Errorf("failed to create manifest file resolver: %w", err) + } + + defaultCRFileResolver, err := fileresolver.NewFileResolver("kyma-module-default-cr-*.yaml", tmpFileSystem) + if err != nil { + return nil, fmt.Errorf("failed to create default CR file resolver: %w", err) + } + + moduleConfigService, err := moduleconfigreader.NewService(fileSystemUtil) if err != nil { return nil, fmt.Errorf("failed to create module config service: %w", err) } @@ -127,7 +138,8 @@ func buildModuleService() (*create.Service, error) { return nil, fmt.Errorf("failed to create crd parser service: %w", err) } moduleService, err := create.NewService(moduleConfigService, gitSourcesService, - securityConfigService, componentArchiveService, registryService, moduleTemplateService, crdParserService) + securityConfigService, componentArchiveService, registryService, moduleTemplateService, + crdParserService, manifestFileResolver, defaultCRFileResolver, fileSystemUtil) if err != nil { return nil, fmt.Errorf("failed to create module service: %w", err) } diff --git a/cmd/modulectl/create/example.txt b/cmd/modulectl/create/example.txt index 93bf5e79..1434cd8c 100644 --- a/cmd/modulectl/create/example.txt +++ b/cmd/modulectl/create/example.txt @@ -1,2 +1,2 @@ Build a simple module and push it to a remote registry - modulectl create --module-config-file=/path/to/module-config-file --registry http://localhost:5001/unsigned --insecure \ No newline at end of file + modulectl create --config-file=/path/to/module-config-file --registry http://localhost:5001/unsigned --insecure \ No newline at end of file diff --git a/docs/gen-docs/modulectl_create.md b/docs/gen-docs/modulectl_create.md index c3c9f311..5bf002fa 100644 --- a/docs/gen-docs/modulectl_create.md +++ b/docs/gen-docs/modulectl_create.md @@ -54,7 +54,7 @@ modulectl create [--config-file MODULE_CONFIG_FILE] [--registry MODULE_REGISTRY] ```bash Build a simple module and push it to a remote registry - modulectl create --module-config-file=/path/to/module-config-file --registry http://localhost:5001/unsigned --insecure + modulectl create --config-file=/path/to/module-config-file --registry http://localhost:5001/unsigned --insecure ``` ## Flags diff --git a/go.mod b/go.mod index 1b136d5c..1e435fcd 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,6 @@ module github.com/kyma-project/modulectl go 1.23.2 -// This replace is because the new ocm package version doesn't allow pushing to insecure registries. https://github.com/open-component-model/ocm/issues/971 -replace ocm.software/ocm => ocm.software/ocm v0.15.0 - require ( github.com/Masterminds/semver/v3 v3.3.0 github.com/go-git/go-git/v5 v5.12.0 @@ -18,7 +15,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 k8s.io/apiextensions-apiserver v0.31.1 k8s.io/apimachinery v0.31.1 - ocm.software/ocm v0.16.0 + ocm.software/ocm v0.16.2 sigs.k8s.io/yaml v1.4.0 ) @@ -230,7 +227,6 @@ require ( github.com/opencontainers/runtime-spec v1.2.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pborman/uuid v1.2.1 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect @@ -335,6 +331,7 @@ require ( k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect oras.land/oras-go v1.2.5 // indirect + oras.land/oras-go/v2 v2.5.0 // indirect sigs.k8s.io/controller-runtime v0.19.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.17.3 // indirect diff --git a/go.sum b/go.sum index 5e0ab8d2..339045c5 100644 --- a/go.sum +++ b/go.sum @@ -379,8 +379,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fluxcd/cli-utils v0.36.0-flux.9 h1:RITKdwIAqT3EFKXl7B91mj6usVjxcy7W8PJZlxqUa84= github.com/fluxcd/cli-utils v0.36.0-flux.9/go.mod h1:q6lXQpbAlrZmTB4Qe5oAENkv0y2kwMWcqTMDHrRo2Is= -github.com/fluxcd/pkg/ssa v0.41.0 h1:UFrnHJ/cT2+6Qoh98o7INipSoj8GjwMEtb9hLus15xQ= -github.com/fluxcd/pkg/ssa v0.41.0/go.mod h1:Lfu6g8AGbJ/MHSq5zSOBWMTJu9pPC5dG1ykmYC1NTPs= +github.com/fluxcd/pkg/ssa v0.41.1 h1:VW87zsLYAKUvCxJhuEH7VzxVh3SxaU+PyApCT6gKjTk= +github.com/fluxcd/pkg/ssa v0.41.1/go.mod h1:7cbyLHqFd5FpcKvhxbHG3DkMm3cZteW45Mi78B0hg8g= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -512,8 +512,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= -github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= +github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= +github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= @@ -627,9 +627,8 @@ github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++ github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE= github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= -github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= -github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc= github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= @@ -1329,14 +1328,16 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/kubectl v0.31.0 h1:kANwAAPVY02r4U4jARP/C+Q1sssCcN/1p9Nk+7BQKVg= -k8s.io/kubectl v0.31.0/go.mod h1:pB47hhFypGsaHAPjlwrNbvhXgmuAr01ZBvAIIUaI8d4= +k8s.io/kubectl v0.31.1 h1:ih4JQJHxsEggFqDJEHSOdJ69ZxZftgeZvYo7M/cpp24= +k8s.io/kubectl v0.31.1/go.mod h1:aNuQoR43W6MLAtXQ/Bu4GDmoHlbhHKuyD49lmTC8eJM= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -ocm.software/ocm v0.15.0 h1:9DUaVXSh0T2Q0ty7abyoOPhSRqHmM8xw/5/uPf4m1h0= -ocm.software/ocm v0.15.0/go.mod h1:uNul0D9Z/7BrEB1LBnRYXlixjbEqtLMSaOMs/qGg1rk= +ocm.software/ocm v0.16.2 h1:XonVlRRtBUVmDClFxThh0MPwP/70dJAJ44DAxYU+8eU= +ocm.software/ocm v0.16.2/go.mod h1:0AnDbAG3qv6xj0gxOzEiB7n9lNXdni3j31PsjROaZNs= oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= +oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= +oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/internal/common/validation/validation.go b/internal/common/validation/validation.go index 828eb562..7956d28f 100644 --- a/internal/common/validation/validation.go +++ b/internal/common/validation/validation.go @@ -2,12 +2,14 @@ package validation import ( "fmt" + "net/url" "regexp" "strings" "github.com/Masterminds/semver/v3" commonerrors "github.com/kyma-project/modulectl/internal/common/errors" + "github.com/kyma-project/modulectl/internal/service/contentprovider" ) const ( @@ -107,6 +109,37 @@ func ValidateNamespace(namespace string) error { return nil } +func ValidateResources(resources contentprovider.ResourcesMap) error { + for name, link := range resources { + if name == "" { + return fmt.Errorf("%w: name must not be empty", commonerrors.ErrInvalidOption) + } + + if link == "" { + return fmt.Errorf("%w: link must not be empty", commonerrors.ErrInvalidOption) + } + + if err := ValidateIsValidHTTPSURL(link); err != nil { + return err + } + } + + return nil +} + +func ValidateIsValidHTTPSURL(input string) error { + _url, err := url.Parse(input) + if err != nil { + return fmt.Errorf("%w: link %s is not a valid URL", commonerrors.ErrInvalidOption, input) + } + + if _url.Scheme != "https" { + return fmt.Errorf("%w: link %s is not using https scheme", commonerrors.ErrInvalidOption, input) + } + + return nil +} + func validateSemanticVersion(version string) error { _, err := semver.StrictNewVersion(strings.TrimSpace(version)) if err != nil { diff --git a/internal/common/validation/validation_test.go b/internal/common/validation/validation_test.go index adeb11c4..32d6df45 100644 --- a/internal/common/validation/validation_test.go +++ b/internal/common/validation/validation_test.go @@ -1,9 +1,11 @@ package validation_test import ( + "fmt" "testing" "github.com/kyma-project/modulectl/internal/common/validation" + "github.com/kyma-project/modulectl/internal/service/contentprovider" ) func TestValidateModuleName(t *testing.T) { @@ -266,3 +268,96 @@ func TestValidateGvk(t *testing.T) { }) } } + +func TestValidateResources(t *testing.T) { + tests := []struct { + name string + resources contentprovider.ResourcesMap + wantErr bool + }{ + { + name: "valid resources", + resources: contentprovider.ResourcesMap{ + "first": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + "second": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + }, + wantErr: false, + }, + { + name: "empty name", + resources: contentprovider.ResourcesMap{ + "": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + }, + wantErr: true, + }, + { + name: "empty link", + resources: contentprovider.ResourcesMap{ + "first": "", + }, + wantErr: true, + }, + { + name: "non-https schema", + resources: contentprovider.ResourcesMap{ + "first": "http://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validation.ValidateResources(tt.resources); (err != nil) != tt.wantErr { + fmt.Println(err.Error()) + t.Errorf("ValidateResources() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestValidateIsValidHttpsUrl(t *testing.T) { + tests := []struct { + name string + url string + wantErr bool + }{ + { + name: "valid url", + url: "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + wantErr: false, + }, + { + name: "invalid url - not using https", + url: "http://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + wantErr: true, + }, + { + name: "invalid url - usig file scheme", + url: "file:///Users/User/template-operator/releases/download/1.0.1/template-operator.yaml", + wantErr: true, + }, + { + name: "invalid url - local path", + url: "./1.0.1/template-operator.yaml", + wantErr: true, + }, + { + name: "invalid url", + url: "%% not a valid url", + wantErr: true, + }, + { + name: "empty url", + url: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validation.ValidateIsValidHTTPSURL(tt.url); (err != nil) != tt.wantErr { + fmt.Println(err.Error()) + t.Errorf("ValidateIsValidUrl() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/service/contentprovider/moduleconfig.go b/internal/service/contentprovider/moduleconfig.go index 088dfdbf..e3e255ad 100644 --- a/internal/service/contentprovider/moduleconfig.go +++ b/internal/service/contentprovider/moduleconfig.go @@ -1,6 +1,7 @@ package contentprovider import ( + "errors" "fmt" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -9,6 +10,8 @@ import ( "github.com/kyma-project/modulectl/internal/common/types" ) +var ErrDuplicateResourceNames = errors.New("resources contain duplicate entries") + type ModuleConfigProvider struct { yamlConverter ObjectToYAMLConverter } @@ -35,12 +38,12 @@ func (s *ModuleConfigProvider) GetDefaultContent(args types.KeyValueArgs) (strin func (s *ModuleConfigProvider) getModuleConfig(args types.KeyValueArgs) ModuleConfig { return ModuleConfig{ - Name: args[ArgModuleName], - Version: args[ArgModuleVersion], - Channel: args[ArgModuleChannel], - ManifestPath: args[ArgManifestFile], - Security: args[ArgSecurityConfigFile], - DefaultCRPath: args[ArgDefaultCRFile], + Name: args[ArgModuleName], + Version: args[ArgModuleVersion], + Channel: args[ArgModuleChannel], + Manifest: args[ArgManifestFile], + Security: args[ArgSecurityConfigFile], + DefaultCR: args[ArgDefaultCRFile], } } @@ -92,4 +95,38 @@ type ModuleConfig struct { Annotations map[string]string `yaml:"annotations" comment:"optional, additional annotations for the ModuleTemplate"` AssociatedResources []*metav1.GroupVersionKind `yaml:"associatedResources" comment:"optional, GVK of the resources which are associated to the ModuleTemplate"` Manager *Manager `yaml:"manager" comment:"optional, the module resource that can be used to indicate the installation readiness of the module. This is typically the manager deployment of the module"` + Resources ResourcesMap `yaml:"resources,omitempty" comment:"optional, additional resources of the ModuleTemplate that may be fetched"` +} + +type resource struct { + Name string `yaml:"name"` + Link string `yaml:"link"` +} + +type ResourcesMap map[string]string + +func (rm *ResourcesMap) UnmarshalYAML(unmarshal func(interface{}) error) error { + resources := []resource{} + if err := unmarshal(&resources); err != nil { + return err + } + + *rm = make(map[string]string) + for _, resource := range resources { + (*rm)[resource.Name] = resource.Link + } + + if len(resources) > len(*rm) { + return ErrDuplicateResourceNames + } + + return nil +} + +func (rm ResourcesMap) MarshalYAML() (interface{}, error) { + resources := []resource{} + for name, link := range rm { + resources = append(resources, resource{Name: name, Link: link}) + } + return resources, nil } diff --git a/internal/service/contentprovider/moduleconfig_test.go b/internal/service/contentprovider/moduleconfig_test.go index 4647e971..31e67e2e 100644 --- a/internal/service/contentprovider/moduleconfig_test.go +++ b/internal/service/contentprovider/moduleconfig_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" commonerrors "github.com/kyma-project/modulectl/internal/common/errors" "github.com/kyma-project/modulectl/internal/common/types" @@ -134,6 +135,86 @@ func Test_ModuleConfig_GetDefaultContent_ReturnsConvertedContent(t *testing.T) { assert.Equal(t, mcConvertedContent, result) } +func Test_ModuleConfig_Unmarshall_Resources_Success(t *testing.T) { + moduleConfigData := ` +resources: + - name: resource1 + link: https://example.com/resource1 + - name: resource2 + link: https://example.com/resource2 +` + + moduleConfig := &contentprovider.ModuleConfig{} + err := yaml.Unmarshal([]byte(moduleConfigData), moduleConfig) + + require.NoError(t, err) + assert.Len(t, moduleConfig.Resources, 2) + assert.Equal(t, "https://example.com/resource1", moduleConfig.Resources["resource1"]) + assert.Equal(t, "https://example.com/resource2", moduleConfig.Resources["resource2"]) +} + +func Test_ModuleConfig_Unmarshall_Resources_Success_Ignoring_Unknown_Fields(t *testing.T) { + moduleConfigData := ` +resources: + - name: resource1 + link: https://example.com/resource1 + unknown: something +` + + moduleConfig := &contentprovider.ModuleConfig{} + err := yaml.Unmarshal([]byte(moduleConfigData), moduleConfig) + + require.NoError(t, err) + assert.Len(t, moduleConfig.Resources, 1) + assert.Equal(t, "https://example.com/resource1", moduleConfig.Resources["resource1"]) +} + +func Test_ModuleConfig_Unmarshall_Resources_FailOnDuplicateNames(t *testing.T) { + moduleConfigData := ` +resources: + - name: resource1 + link: https://example.com/resource1 + - name: resource1 + link: https://example.com/resource1 +` + + moduleConfig := &contentprovider.ModuleConfig{} + err := yaml.Unmarshal([]byte(moduleConfigData), moduleConfig) + + require.Error(t, err) + assert.Equal(t, "resources contain duplicate entries", err.Error()) +} + +func Test_ModuleConfig_Marshall_Resources_Success(t *testing.T) { + // parse the expected config + expectedModuleConfigData := ` +resources: + - name: resource1 + link: https://example.com/resource1 + - name: resource2 + link: https://example.com/resource2 +` + expectedModuleConfig := &contentprovider.ModuleConfig{} + err := yaml.Unmarshal([]byte(expectedModuleConfigData), expectedModuleConfig) + require.NoError(t, err) + + // round trip a module config (marshal and unmarshal) + moduleConfig := &contentprovider.ModuleConfig{ + Resources: contentprovider.ResourcesMap{ + "resource1": "https://example.com/resource1", + "resource2": "https://example.com/resource2", + }, + } + marshalledModuleConfigData, err := yaml.Marshal(moduleConfig) + require.NoError(t, err) + + roudTrippedModuleConfig := &contentprovider.ModuleConfig{} + err = yaml.Unmarshal(marshalledModuleConfigData, roudTrippedModuleConfig) + + require.NoError(t, err) + assert.Equal(t, expectedModuleConfig.Resources, roudTrippedModuleConfig.Resources) +} + // Test Stubs type mcObjectToYAMLConverterStub struct{} diff --git a/internal/service/create/create.go b/internal/service/create/create.go index 8527911d..7a50ce38 100644 --- a/internal/service/create/create.go +++ b/internal/service/create/create.go @@ -15,7 +15,14 @@ import ( type ModuleConfigService interface { ParseAndValidateModuleConfig(moduleConfigFile string) (*contentprovider.ModuleConfig, error) - GetDefaultCRData(defaultCRPath string) ([]byte, error) +} + +type FileSystem interface { + ReadFile(path string) ([]byte, error) +} + +type FileResolver interface { + Resolve(file string) (string, error) CleanupTempFiles() []error } @@ -62,6 +69,9 @@ type Service struct { registryService RegistryService moduleTemplateService ModuleTemplateService crdParserService CRDParserService + manifestFileResolver FileResolver + defaultCRFileResolver FileResolver + fileSystem FileSystem } func NewService(moduleConfigService ModuleConfigService, @@ -71,6 +81,9 @@ func NewService(moduleConfigService ModuleConfigService, registryService RegistryService, moduleTemplateService ModuleTemplateService, crdParserService CRDParserService, + manifestFileResolver FileResolver, + defaultCRFileResolver FileResolver, + fileSystem FileSystem, ) (*Service, error) { if moduleConfigService == nil { return nil, fmt.Errorf("%w: moduleConfigService must not be nil", commonerrors.ErrInvalidArg) @@ -100,6 +113,18 @@ func NewService(moduleConfigService ModuleConfigService, return nil, fmt.Errorf("%w: crdParserService must not be nil", commonerrors.ErrInvalidArg) } + if manifestFileResolver == nil { + return nil, fmt.Errorf("%w: manifestFileResolver must not be nil", commonerrors.ErrInvalidArg) + } + + if defaultCRFileResolver == nil { + return nil, fmt.Errorf("%w: defaultCRFileResolver must not be nil", commonerrors.ErrInvalidArg) + } + + if fileSystem == nil { + return nil, fmt.Errorf("%w: fileSystem must not be nil", commonerrors.ErrInvalidArg) + } + return &Service{ moduleConfigService: moduleConfigService, gitSourcesService: gitSourcesService, @@ -108,6 +133,9 @@ func NewService(moduleConfigService ModuleConfigService, registryService: registryService, moduleTemplateService: moduleTemplateService, crdParserService: crdParserService, + manifestFileResolver: manifestFileResolver, + defaultCRFileResolver: defaultCRFileResolver, + fileSystem: fileSystem, }, nil } @@ -117,8 +145,12 @@ func (s *Service) Run(opts Options) error { } defer func() { - if err := s.moduleConfigService.CleanupTempFiles(); err != nil { - opts.Out.Write(fmt.Sprintf("failed to cleanup temporary files: %v\n", err)) + if err := s.defaultCRFileResolver.CleanupTempFiles(); err != nil { + opts.Out.Write(fmt.Sprintf("failed to cleanup temporary default CR files: %v\n", err)) + } + + if err := s.manifestFileResolver.CleanupTempFiles(); err != nil { + opts.Out.Write(fmt.Sprintf("failed to cleanup temporary manifest files: %v\n", err)) } }() @@ -127,13 +159,26 @@ func (s *Service) Run(opts Options) error { return fmt.Errorf("failed to parse module config: %w", err) } + manifestFilePath, err := s.manifestFileResolver.Resolve(moduleConfig.Manifest) + if err != nil { + return fmt.Errorf("failed to resolve manifest file: %w", err) + } + + defaultCRFilePath := moduleConfig.DefaultCR + if moduleConfig.DefaultCR != "" { + defaultCRFilePath, err = s.defaultCRFileResolver.Resolve(moduleConfig.DefaultCR) + if err != nil { + return fmt.Errorf("failed to resolve default CR file: %w", err) + } + } + descriptor, err := componentdescriptor.InitializeComponentDescriptor(moduleConfig.Name, moduleConfig.Version) if err != nil { return fmt.Errorf("failed to populate component descriptor metadata: %w", err) } - moduleResources, err := componentdescriptor.GenerateModuleResources(moduleConfig.Version, moduleConfig.ManifestPath, - moduleConfig.DefaultCRPath, opts.RegistryCredSelector) + moduleResources, err := componentdescriptor.GenerateModuleResources(moduleConfig.Version, manifestFilePath, + defaultCRFilePath, opts.RegistryCredSelector) if err != nil { return fmt.Errorf("failed to generate module resources: %w", err) } @@ -162,14 +207,14 @@ func (s *Service) Run(opts Options) error { } if opts.RegistryURL != "" { - return s.pushImgAndCreateTemplate(archive, moduleConfig, opts) + return s.pushImgAndCreateTemplate(archive, moduleConfig, manifestFilePath, defaultCRFilePath, opts) } return nil } -func (s *Service) pushImgAndCreateTemplate(archive *comparch.ComponentArchive, moduleConfig *contentprovider.ModuleConfig, opts Options) error { +func (s *Service) pushImgAndCreateTemplate(archive *comparch.ComponentArchive, moduleConfig *contentprovider.ModuleConfig, manifestFilePath, defaultCRFilePath string, opts Options) error { opts.Out.Write("- Pushing component version\n") - isCRDClusterScoped, err := s.crdParserService.IsCRDClusterScoped(moduleConfig.DefaultCRPath, moduleConfig.ManifestPath) + isCRDClusterScoped, err := s.crdParserService.IsCRDClusterScoped(defaultCRFilePath, manifestFilePath) if err != nil { return fmt.Errorf("failed to determine if CRD is cluster scoped: %w", err) } @@ -185,8 +230,8 @@ func (s *Service) pushImgAndCreateTemplate(archive *comparch.ComponentArchive, m } var crData []byte - if moduleConfig.DefaultCRPath != "" { - crData, err = s.moduleConfigService.GetDefaultCRData(moduleConfig.DefaultCRPath) + if defaultCRFilePath != "" { + crData, err = s.fileSystem.ReadFile(defaultCRFilePath) if err != nil { return fmt.Errorf("%w: failed to get default CR data", err) } diff --git a/internal/service/create/create_test.go b/internal/service/create/create_test.go index c49078a6..8d01f849 100644 --- a/internal/service/create/create_test.go +++ b/internal/service/create/create_test.go @@ -20,7 +20,8 @@ import ( func Test_NewService_ReturnsError_WhenModuleConfigServiceIsNil(t *testing.T) { _, err := create.NewService(nil, &gitSourcesServiceStub{}, &securityConfigServiceStub{}, - &componentArchiveServiceStub{}, ®istryServiceStub{}, &ModuleTemplateServiceStub{}, &CRDParserServiceStub{}) + &componentArchiveServiceStub{}, ®istryServiceStub{}, &ModuleTemplateServiceStub{}, &CRDParserServiceStub{}, + &fileResolverStub{}, &fileResolverStub{}, &fileExistsStub{}) require.ErrorIs(t, err, commonerrors.ErrInvalidArg) require.Contains(t, err.Error(), "moduleConfigService") @@ -28,7 +29,8 @@ func Test_NewService_ReturnsError_WhenModuleConfigServiceIsNil(t *testing.T) { func Test_CreateModule_ReturnsError_WhenModuleConfigFileIsEmpty(t *testing.T) { svc, err := create.NewService(&moduleConfigServiceStub{}, &gitSourcesServiceStub{}, &securityConfigServiceStub{}, - &componentArchiveServiceStub{}, ®istryServiceStub{}, &ModuleTemplateServiceStub{}, &CRDParserServiceStub{}) + &componentArchiveServiceStub{}, ®istryServiceStub{}, &ModuleTemplateServiceStub{}, &CRDParserServiceStub{}, + &fileResolverStub{}, &fileResolverStub{}, &fileExistsStub{}) require.NoError(t, err) opts := newCreateOptionsBuilder().withModuleConfigFile("").build() @@ -41,7 +43,8 @@ func Test_CreateModule_ReturnsError_WhenModuleConfigFileIsEmpty(t *testing.T) { func Test_CreateModule_ReturnsError_WhenOutIsNil(t *testing.T) { svc, err := create.NewService(&moduleConfigServiceStub{}, &gitSourcesServiceStub{}, &securityConfigServiceStub{}, - &componentArchiveServiceStub{}, ®istryServiceStub{}, &ModuleTemplateServiceStub{}, &CRDParserServiceStub{}) + &componentArchiveServiceStub{}, ®istryServiceStub{}, &ModuleTemplateServiceStub{}, &CRDParserServiceStub{}, + &fileResolverStub{}, &fileResolverStub{}, &fileExistsStub{}) require.NoError(t, err) opts := newCreateOptionsBuilder().withOut(nil).build() @@ -54,7 +57,8 @@ func Test_CreateModule_ReturnsError_WhenOutIsNil(t *testing.T) { func Test_CreateModule_ReturnsError_WhenCredentialsIsInInvalidFormat(t *testing.T) { svc, err := create.NewService(&moduleConfigServiceStub{}, &gitSourcesServiceStub{}, &securityConfigServiceStub{}, - &componentArchiveServiceStub{}, ®istryServiceStub{}, &ModuleTemplateServiceStub{}, &CRDParserServiceStub{}) + &componentArchiveServiceStub{}, ®istryServiceStub{}, &ModuleTemplateServiceStub{}, &CRDParserServiceStub{}, + &fileResolverStub{}, &fileResolverStub{}, &fileExistsStub{}) require.NoError(t, err) opts := newCreateOptionsBuilder().withCredentials("user").build() @@ -67,7 +71,8 @@ func Test_CreateModule_ReturnsError_WhenCredentialsIsInInvalidFormat(t *testing. func Test_CreateModule_ReturnsError_WhenTemplateOutputIsEmpty(t *testing.T) { svc, err := create.NewService(&moduleConfigServiceStub{}, &gitSourcesServiceStub{}, &securityConfigServiceStub{}, - &componentArchiveServiceStub{}, ®istryServiceStub{}, &ModuleTemplateServiceStub{}, &CRDParserServiceStub{}) + &componentArchiveServiceStub{}, ®istryServiceStub{}, &ModuleTemplateServiceStub{}, &CRDParserServiceStub{}, + &fileResolverStub{}, &fileResolverStub{}, &fileExistsStub{}) require.NoError(t, err) opts := newCreateOptionsBuilder().withTemplateOutput("").build() @@ -79,9 +84,9 @@ func Test_CreateModule_ReturnsError_WhenTemplateOutputIsEmpty(t *testing.T) { } func Test_CreateModule_ReturnsError_WhenParseAndValidateModuleConfigReturnsError(t *testing.T) { - svc, err := create.NewService(&moduleConfigServiceParseErrorStub{}, &gitSourcesServiceStub{}, - &securityConfigServiceStub{}, &componentArchiveServiceStub{}, ®istryServiceStub{}, - &ModuleTemplateServiceStub{}, &CRDParserServiceStub{}) + svc, err := create.NewService(&moduleConfigServiceParseErrorStub{}, &gitSourcesServiceStub{}, &securityConfigServiceStub{}, + &componentArchiveServiceStub{}, ®istryServiceStub{}, &ModuleTemplateServiceStub{}, &CRDParserServiceStub{}, + &fileResolverStub{}, &fileResolverStub{}, &fileExistsStub{}) require.NoError(t, err) opts := newCreateOptionsBuilder().build() @@ -92,6 +97,34 @@ func Test_CreateModule_ReturnsError_WhenParseAndValidateModuleConfigReturnsError require.Contains(t, err.Error(), "failed to read module config file") } +func Test_CreateModule_ReturnsError_WhenResolvingManifestFilePathReturnsError(t *testing.T) { + svc, err := create.NewService(&moduleConfigServiceStub{}, &gitSourcesServiceStub{}, &securityConfigServiceStub{}, + &componentArchiveServiceStub{}, ®istryServiceStub{}, &ModuleTemplateServiceStub{}, &CRDParserServiceStub{}, + &fileResolverErrorStub{}, &fileResolverStub{}, &fileExistsStub{}) + require.NoError(t, err) + + opts := newCreateOptionsBuilder().build() + + err = svc.Run(opts) + + require.Error(t, err) + require.Contains(t, err.Error(), "failed to resolve file") +} + +func Test_CreateModule_ReturnsError_WhenResolvingDefaultCRFilePathReturnsError(t *testing.T) { + svc, err := create.NewService(&moduleConfigServiceStub{}, &gitSourcesServiceStub{}, &securityConfigServiceStub{}, + &componentArchiveServiceStub{}, ®istryServiceStub{}, &ModuleTemplateServiceStub{}, &CRDParserServiceStub{}, + &fileResolverStub{}, &fileResolverErrorStub{}, &fileExistsStub{}) + require.NoError(t, err) + + opts := newCreateOptionsBuilder().build() + + err = svc.Run(opts) + + require.Error(t, err) + require.Contains(t, err.Error(), "failed to resolve file") +} + type createOptionsBuilder struct { options create.Options } @@ -144,21 +177,44 @@ func (b *createOptionsBuilder) withCredentials(credentials string) *createOption return b } -// Test Stubs -type moduleConfigServiceStub struct{} +type fileExistsStub struct{} -func (*moduleConfigServiceStub) ParseAndValidateModuleConfig(_ string) (*contentprovider.ModuleConfig, error) { - return &contentprovider.ModuleConfig{}, nil +func (*fileExistsStub) FileExists(_ string) (bool, error) { + return true, nil } -func (*moduleConfigServiceStub) GetDefaultCRData(_ string) ([]byte, error) { - return []byte{}, nil +func (*fileExistsStub) ReadFile(_ string) ([]byte, error) { + return nil, nil +} + +type fileResolverStub struct{} + +func (*fileResolverStub) Resolve(_ string) (string, error) { + return "/tmp/some-file.yaml", nil } -func (*moduleConfigServiceStub) CleanupTempFiles() []error { +func (*fileResolverStub) CleanupTempFiles() []error { return nil } +type fileResolverErrorStub struct{} + +func (*fileResolverErrorStub) Resolve(_ string) (string, error) { + return "", errors.New("failed to resolve file") +} + +func (*fileResolverErrorStub) CleanupTempFiles() []error { + return []error{errors.New("failed to cleanup temp files")} +} + +type moduleConfigServiceStub struct{} + +func (*moduleConfigServiceStub) ParseAndValidateModuleConfig(_ string) (*contentprovider.ModuleConfig, error) { + return &contentprovider.ModuleConfig{ + DefaultCR: "default-cr.yaml", + }, nil +} + type moduleConfigServiceParseErrorStub struct{} func (*moduleConfigServiceParseErrorStub) ParseAndValidateModuleConfig(_ string) (*contentprovider.ModuleConfig, @@ -167,14 +223,6 @@ func (*moduleConfigServiceParseErrorStub) ParseAndValidateModuleConfig(_ string) return nil, errors.New("failed to read module config file") } -func (*moduleConfigServiceParseErrorStub) GetDefaultCRData(_ string) ([]byte, error) { - return []byte{}, nil -} - -func (*moduleConfigServiceParseErrorStub) CleanupTempFiles() []error { - return nil -} - type gitSourcesServiceStub struct{} func (*gitSourcesServiceStub) AddGitSources(_ *compdesc.ComponentDescriptor, diff --git a/internal/service/fileresolver/fileresolver.go b/internal/service/fileresolver/fileresolver.go new file mode 100644 index 00000000..e0c2cbcb --- /dev/null +++ b/internal/service/fileresolver/fileresolver.go @@ -0,0 +1,74 @@ +package fileresolver + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + + commonerrors "github.com/kyma-project/modulectl/internal/common/errors" +) + +type TempFileSystem interface { + DownloadTempFile(dir, pattern string, url *url.URL) (string, error) + RemoveTempFiles() []error +} + +type FileResolver struct { + filePattern string + tempFileSystem TempFileSystem +} + +func NewFileResolver(filePattern string, tempFileSystem TempFileSystem) (*FileResolver, error) { + if filePattern == "" { + return nil, fmt.Errorf("%w: filePattern must not be empty", commonerrors.ErrInvalidArg) + } + + if tempFileSystem == nil { + return nil, fmt.Errorf("%w: tempFileSystem must not be nil", commonerrors.ErrInvalidArg) + } + + return &FileResolver{ + filePattern: filePattern, + tempFileSystem: tempFileSystem, + }, nil +} + +func (r *FileResolver) Resolve(file string) (string, error) { + if parsedURL, err := r.ParseURL(file); err == nil { + file, err = r.tempFileSystem.DownloadTempFile("", r.filePattern, parsedURL) + if err != nil { + return "", fmt.Errorf("failed to download file: %w", err) + } + return file, nil + } + + if !filepath.IsAbs(file) { + // Get the current working directory + homeDir, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get the current directory: %w", err) + } + // Get the relative path from the current directory + file = filepath.Join(homeDir, file) + file, err = filepath.Abs(file) + if err != nil { + return "", fmt.Errorf("failed to obtain absolute path to file: %w", err) + } + return file, nil + } + + return file, nil +} + +func (r *FileResolver) 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("failed to parse url %s: %w", urlString, commonerrors.ErrInvalidArg) +} + +func (r *FileResolver) CleanupTempFiles() []error { + return r.tempFileSystem.RemoveTempFiles() +} diff --git a/internal/service/fileresolver/fileresolver_test.go b/internal/service/fileresolver/fileresolver_test.go new file mode 100644 index 00000000..794fe29a --- /dev/null +++ b/internal/service/fileresolver/fileresolver_test.go @@ -0,0 +1,145 @@ +package fileresolver_test + +import ( + "fmt" + "net/url" + "testing" + + "github.com/mandelsoft/goutils/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + commonerrors "github.com/kyma-project/modulectl/internal/common/errors" + "github.com/kyma-project/modulectl/internal/service/fileresolver" +) + +const filePattern = "kyma-module-manifest-*.yaml" + +func TestNew_CalledWithEmptyFilePattern_ReturnsErr(t *testing.T) { + _, err := fileresolver.NewFileResolver("", &tmpfileSystemStub{}) + require.ErrorIs(t, err, commonerrors.ErrInvalidArg) + assert.Contains(t, err.Error(), "filePattern must not be empty") +} + +func TestNew_CalledWithNilDependencies_ReturnsErr(t *testing.T) { + _, err := fileresolver.NewFileResolver(filePattern, nil) + require.ErrorIs(t, err, commonerrors.ErrInvalidArg) + assert.Contains(t, err.Error(), "tempFileSystem must not be nil") +} + +func TestCleanupTempFiles_CalledWithNoTempFiles_ReturnsNoErrors(t *testing.T) { + resolver, _ := fileresolver.NewFileResolver(filePattern, &tmpfileSystemStub{}) + + errs := resolver.CleanupTempFiles() + assert.Empty(t, errs) +} + +func Test_Resolve_Returns_CorrectPath(t *testing.T) { + resolver, _ := fileresolver.NewFileResolver(filePattern, &tmpfileSystemStub{}) + result, err := resolver.Resolve("https://example.com/path") + + require.NoError(t, err) + require.Equal(t, "file.yaml", result) +} + +func Test_Resolve_Returns_Error_WhenFailingToDownload(t *testing.T) { + resolver, _ := fileresolver.NewFileResolver(filePattern, &tempfileSystemErrorStub{}) + result, err := resolver.Resolve("https://example.com/path") + + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to download file") + assert.Empty(t, result) +} + +func Test_Resolve_Returns_CorrectPath_When_AbsolutePath(t *testing.T) { + resolver, _ := fileresolver.NewFileResolver(filePattern, &tmpfileSystemStub{}) + result, err := resolver.Resolve("/path/to/manifest.yaml") + + require.NoError(t, err) + assert.Equal(t, "/path/to/manifest.yaml", result) +} + +func Test_Resolve_Returns_CorrectPath_When_Relative(t *testing.T) { + resolver, _ := fileresolver.NewFileResolver(filePattern, &tmpfileSystemStub{}) + result, err := resolver.Resolve("./path/to/manifest.yaml") + + require.NoError(t, err) + assert.Contains(t, result, "/path/to/manifest.yaml") + assert.Equal(t, '/', rune(result[0])) +} + +func TestService_ParseURL(t *testing.T) { + tests := []struct { + name string + urlString string + want *url.URL + expectedError error + }{ + { + name: "valid URL", + urlString: "https://example.com/path", + want: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/path", + }, + expectedError: nil, + }, + { + name: "invalid URL", + urlString: "invalid-url", + want: nil, + expectedError: fmt.Errorf("failed to parse url invalid-url: %w", commonerrors.ErrInvalidArg), + }, + { + name: "URL without Scheme", + urlString: "example.com/path", + want: nil, + expectedError: fmt.Errorf("failed to parse url example.com/path: %w", commonerrors.ErrInvalidArg), + }, + { + name: "URL without Host", + urlString: "https://", + want: nil, + expectedError: fmt.Errorf("failed to parse url https://: %w", commonerrors.ErrInvalidArg), + }, + { + name: "Empty URL", + urlString: "", + want: nil, + expectedError: fmt.Errorf("failed to parse url : %w", commonerrors.ErrInvalidArg), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + resolver, _ := fileresolver.NewFileResolver(filePattern, &tmpfileSystemStub{}) + got, err := resolver.ParseURL(test.urlString) + + if test.expectedError != nil { + require.EqualError(t, err, test.expectedError.Error()) + return + } + require.Equalf(t, test.want, got, "ParseURL(%v)", test.urlString) + }) + } +} + +type tmpfileSystemStub struct{} + +func (*tmpfileSystemStub) DownloadTempFile(_ string, _ string, _ *url.URL) (string, error) { + return "file.yaml", nil +} + +func (s *tmpfileSystemStub) RemoveTempFiles() []error { + return nil +} + +type tempfileSystemErrorStub struct{} + +func (*tempfileSystemErrorStub) DownloadTempFile(_ string, _ string, _ *url.URL) (string, error) { + return "", errors.New("error downloading file") +} + +func (s *tempfileSystemErrorStub) RemoveTempFiles() []error { + return nil +} diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader.go b/internal/service/moduleconfig/reader/moduleconfig_reader.go index f64641c0..591dab96 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader.go @@ -3,9 +3,6 @@ package moduleconfigreader import ( "errors" "fmt" - "net/url" - "os" - "path/filepath" "gopkg.in/yaml.v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,37 +14,21 @@ import ( var ErrNoPathForDefaultCR = errors.New("no path for default CR given") -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 - tempFileSystem TempFileSystem + fileSystem FileSystem } -func NewService(fileSystem FileSystem, tmpFileSystem TempFileSystem) (*Service, error) { +func NewService(fileSystem FileSystem) (*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: tempFileSystem must not be nil", commonerrors.ErrInvalidArg) - } - return &Service{ - fileSystem: fileSystem, - tempFileSystem: tmpFileSystem, + fileSystem: fileSystem, }, nil } @@ -59,75 +40,12 @@ func (s *Service) ParseAndValidateModuleConfig(moduleConfigFile string, } if err = ValidateModuleConfig(moduleConfig); err != nil { - return nil, fmt.Errorf("failed to value module config: %w", err) - } - - moduleConfig.DefaultCRPath, err = GetDefaultCRPath(moduleConfig.DefaultCRPath, s.tempFileSystem) - if err != nil { - return nil, fmt.Errorf("failed to get default CR path: %w", err) - } - - moduleConfig.ManifestPath, err = GetManifestPath(moduleConfig.ManifestPath, s.tempFileSystem) - if err != nil { - return nil, fmt.Errorf("failed to get manifest path: %w", err) + return nil, fmt.Errorf("failed to validate module config: %w", err) } return moduleConfig, nil } -func (s *Service) GetDefaultCRData(defaultCRPath string) ([]byte, error) { - if defaultCRPath == "" { - return nil, ErrNoPathForDefaultCR - } - defaultCRData, err := s.fileSystem.ReadFile(defaultCRPath) - if err != nil { - return nil, fmt.Errorf("failed to read default CR file: %w", err) - } - - return defaultCRData, nil -} - -func (s *Service) CleanupTempFiles() []error { - return s.tempFileSystem.RemoveTempFiles() -} - -func GetManifestPath(manifestPath string, tempFileSystem TempFileSystem) (string, error) { - path := manifestPath - - if parsedURL, err := ParseURL(manifestPath); err == nil { - path, err = tempFileSystem.DownloadTempFile("", defaultManifestFilePattern, parsedURL) - if err != nil { - return "", fmt.Errorf("failed to download Manifest file: %w", err) - } - return path, nil - } - - if !filepath.IsAbs(manifestPath) { - // Get the current working directory - homeDir, err := os.Getwd() - if err != nil { - return "", fmt.Errorf("failed to get the current directory: %w", err) - } - // Get the relative path from the current directory - path = filepath.Join(homeDir, path) - path, err = filepath.Abs(path) - if err != nil { - return "", fmt.Errorf("failed to obtain absolute path to manifest file: %w", err) - } - return path, nil - } - - return path, nil -} - -func 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("failed to parse url %s: %w", urlString, commonerrors.ErrInvalidArg) -} - func ValidateModuleConfig(moduleConfig *contentprovider.ModuleConfig) error { if err := validation.ValidateModuleName(moduleConfig.Name); err != nil { return fmt.Errorf("failed to validate module name: %w", err) @@ -145,8 +63,12 @@ func ValidateModuleConfig(moduleConfig *contentprovider.ModuleConfig) error { return fmt.Errorf("failed to validate module namespace: %w", err) } - if moduleConfig.ManifestPath == "" { - return fmt.Errorf("manifest path must not be empty: %w", commonerrors.ErrInvalidOption) + if err := validation.ValidateResources(moduleConfig.Resources); err != nil { + return fmt.Errorf("failed to validate resources: %w", err) + } + + if moduleConfig.Manifest == "" { + return fmt.Errorf("manifest must not be empty: %w", commonerrors.ErrInvalidOption) } if err := validateAssociatedResources(moduleConfig.AssociatedResources); err != nil { @@ -208,36 +130,3 @@ func ParseModuleConfig(configFilePath string, fileSystem FileSystem) (*contentpr return moduleConfig, nil } - -func GetDefaultCRPath(defaultCRPath string, tempFileSystem TempFileSystem) (string, error) { - if defaultCRPath == "" { - return defaultCRPath, nil - } - - path := defaultCRPath - - if parsedURL, err := ParseURL(defaultCRPath); err == nil { - path, err = tempFileSystem.DownloadTempFile("", defaultCRFilePattern, parsedURL) - if err != nil { - return "", fmt.Errorf("failed to download default CR file: %w", err) - } - return path, nil - } - - if !filepath.IsAbs(defaultCRPath) { - // Get the current working directory - homeDir, err := os.Getwd() - if err != nil { - return "", fmt.Errorf("failed to get the current working directory: %w", err) - } - // Get the relative path from the current directory - path = filepath.Join(homeDir, path) - path, err = filepath.Abs(path) - if err != nil { - return "", fmt.Errorf("failed to obtain absolute path to deefault CR file: %w", err) - } - return path, nil - } - - return path, nil -} diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go index d7a8c473..c3c1650e 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go @@ -3,7 +3,6 @@ package moduleconfigreader_test import ( "errors" "fmt" - "net/url" "testing" "github.com/stretchr/testify/require" @@ -33,8 +32,8 @@ func Test_ParseModuleConfig_Returns_CorrectModuleConfig(t *testing.T) { require.Equal(t, "github.com/module-name", result.Name) require.Equal(t, "0.0.1", result.Version) require.Equal(t, "regular", result.Channel) - require.Equal(t, "path/to/manifests", result.ManifestPath) - require.Equal(t, "path/to/defaultCR", result.DefaultCRPath) + require.Equal(t, "path/to/manifests", result.Manifest) + require.Equal(t, "path/to/defaultCR", result.DefaultCR) require.Equal(t, "module-name-0.0.1", result.ResourceName) require.False(t, result.Mandatory) require.Equal(t, "kcp-system", result.Namespace) @@ -46,6 +45,9 @@ func Test_ParseModuleConfig_Returns_CorrectModuleConfig(t *testing.T) { require.Equal(t, "networking.istio.io", result.AssociatedResources[0].Group) require.Equal(t, "v1alpha3", result.AssociatedResources[0].Version) require.Equal(t, "Gateway", result.AssociatedResources[0].Kind) + require.Equal(t, contentprovider.ResourcesMap{ + "rawManifest": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + }, result.Resources) require.Equal(t, "manager-name", result.Manager.Name) require.Equal(t, "manager-namespace", result.Manager.Namespace) require.Equal(t, "apps", result.Manager.GroupVersionKind.Group) @@ -54,124 +56,8 @@ func Test_ParseModuleConfig_Returns_CorrectModuleConfig(t *testing.T) { } func TestNew_CalledWithNilDependencies_ReturnsErr(t *testing.T) { - _, err := moduleconfigreader.NewService( - nil, - &tmpfileSystemStub{}) + _, err := moduleconfigreader.NewService(nil) require.Error(t, err) - - _, err = moduleconfigreader.NewService( - &fileExistsStub{}, - nil) - require.Error(t, err) -} - -func Test_GetDefaultCRData_CalledWithEmptyPath_ReturnsErr(t *testing.T) { - moduleConfigService, err := moduleconfigreader.NewService( - &fileExistsStub{}, - &tmpfileSystemStub{}) - require.NoError(t, err) - - _, err = moduleConfigService.GetDefaultCRData("") - - require.Error(t, err) - require.ErrorIs(t, err, moduleconfigreader.ErrNoPathForDefaultCR) -} - -func Test_GetDefaultCRData_Returns_CorrectData(t *testing.T) { - moduleConfigService, err := moduleconfigreader.NewService( - &fileExistsStub{}, - &tmpfileSystemStub{}) - require.NoError(t, err) - - result, err := moduleConfigService.GetDefaultCRData("/path/to/defaultcr") - require.NoError(t, err) - - expected, err := yaml.Marshal(expectedReturnedModuleConfig) - require.NoError(t, err) - require.Equal(t, expected, result) -} - -func Test_GetDefaultCRPath_Returns_CorrectPath(t *testing.T) { - result, err := moduleconfigreader.GetDefaultCRPath("https://example.com/path", &tmpfileSystemStub{}) - - require.NoError(t, err) - require.Equal(t, "file.yaml", result) -} - -func Test_GetDefaultCRPath_Returns_CorrectPath_When_NotUrl(t *testing.T) { - result, err := moduleconfigreader.GetDefaultCRPath("/path/to/defaultcr.yaml", &tmpfileSystemStub{}) - - require.NoError(t, err) - require.Equal(t, "/path/to/defaultcr.yaml", result) -} - -func Test_GetManifestPath_Returns_CorrectPath(t *testing.T) { - result, err := moduleconfigreader.GetDefaultCRPath("https://example.com/path", &tmpfileSystemStub{}) - - require.NoError(t, err) - require.Equal(t, "file.yaml", result) -} - -func Test_GetManifestPath_Returns_CorrectPath_When_NotUrl(t *testing.T) { - result, err := moduleconfigreader.GetDefaultCRPath("/path/to/manifest.yaml", &tmpfileSystemStub{}) - - require.NoError(t, err) - require.Equal(t, "/path/to/manifest.yaml", result) -} - -func TestService_ParseURL(t *testing.T) { - tests := []struct { - name string - urlString string - want *url.URL - expectedError error - }{ - { - name: "valid URL", - urlString: "https://example.com/path", - want: &url.URL{ - Scheme: "https", - Host: "example.com", - Path: "/path", - }, - expectedError: nil, - }, - { - name: "invalid URL", - urlString: "invalid-url", - want: nil, - expectedError: fmt.Errorf("failed to parse url invalid-url: %w", commonerrors.ErrInvalidArg), - }, - { - name: "URL without Scheme", - urlString: "example.com/path", - want: nil, - expectedError: fmt.Errorf("failed to parse url example.com/path: %w", commonerrors.ErrInvalidArg), - }, - { - name: "URL without Host", - urlString: "https://", - want: nil, - expectedError: fmt.Errorf("failed to parse url https://: %w", commonerrors.ErrInvalidArg), - }, - { - name: "Empty URL", - urlString: "", - want: nil, - expectedError: fmt.Errorf("failed to parse url : %w", commonerrors.ErrInvalidArg), - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got, err := moduleconfigreader.ParseURL(test.urlString) - - if test.expectedError != nil { - require.EqualError(t, err, test.expectedError.Error()) - return - } - require.Equalf(t, test.want, got, "ParseURL(%v)", test.urlString) - }) - } } func Test_ValidateModuleConfig(t *testing.T) { @@ -188,57 +74,99 @@ func Test_ValidateModuleConfig(t *testing.T) { { name: "invalid module name", moduleConfig: &contentprovider.ModuleConfig{ - Name: "invalid name", - Version: "0.0.1", - Channel: "regular", - Namespace: "kcp-system", - ManifestPath: "test", + Name: "invalid name", + Version: "0.0.1", + Channel: "regular", + Namespace: "kcp-system", + Manifest: "test", }, expectedError: fmt.Errorf("failed to validate module name: %w", commonerrors.ErrInvalidOption), }, { name: "invalid module version", moduleConfig: &contentprovider.ModuleConfig{ - Name: "github.com/module-name", - Version: "invalid version", - Channel: "regular", - Namespace: "kcp-system", - ManifestPath: "test", + Name: "github.com/module-name", + Version: "invalid version", + Channel: "regular", + Namespace: "kcp-system", + Manifest: "test", }, expectedError: fmt.Errorf("failed to validate module version: %w", commonerrors.ErrInvalidOption), }, { name: "invalid module channel", moduleConfig: &contentprovider.ModuleConfig{ - Name: "github.com/module-name", - Version: "0.0.1", - Channel: "invalid channel", - Namespace: "kcp-system", - ManifestPath: "test", + Name: "github.com/module-name", + Version: "0.0.1", + Channel: "invalid channel", + Namespace: "kcp-system", + Manifest: "test", }, expectedError: fmt.Errorf("failed to validate module channel: %w", commonerrors.ErrInvalidOption), }, { name: "invalid module namespace", moduleConfig: &contentprovider.ModuleConfig{ - Name: "github.com/module-name", - Version: "0.0.1", - Channel: "regular", - Namespace: "invalid namespace", - ManifestPath: "test", + Name: "github.com/module-name", + Version: "0.0.1", + Channel: "regular", + Namespace: "invalid namespace", + Manifest: "test", }, expectedError: fmt.Errorf("failed to validate module namespace: %w", commonerrors.ErrInvalidOption), }, { name: "empty manifest path", moduleConfig: &contentprovider.ModuleConfig{ - Name: "github.com/module-name", - Version: "0.0.1", - Channel: "regular", - Namespace: "kcp-system", - ManifestPath: "", + Name: "github.com/module-name", + Version: "0.0.1", + Channel: "regular", + Namespace: "kcp-system", + Manifest: "", }, - expectedError: fmt.Errorf("manifest path must not be empty: %w", commonerrors.ErrInvalidOption), + expectedError: fmt.Errorf("manifest must not be empty: %w", commonerrors.ErrInvalidOption), + }, + { + name: "invalid module resources - not a URL", + moduleConfig: &contentprovider.ModuleConfig{ + Name: "github.com/module-name", + Version: "0.0.1", + Channel: "regular", + Namespace: "kcp-system", + Manifest: "test", + Resources: contentprovider.ResourcesMap{ + "key": "%% not a URL", + }, + }, + expectedError: fmt.Errorf("failed to validate resources: %w: link %%%% not a URL is not a valid URL", commonerrors.ErrInvalidOption), + }, + { + name: "invalid module resources - empty name", + moduleConfig: &contentprovider.ModuleConfig{ + Name: "github.com/module-name", + Version: "0.0.1", + Channel: "regular", + Namespace: "kcp-system", + Manifest: "test", + Resources: contentprovider.ResourcesMap{ + "": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + }, + }, + expectedError: fmt.Errorf("failed to validate resources: %w: name must not be empty", commonerrors.ErrInvalidOption), + }, + { + name: "invalid module resources - empty link", + moduleConfig: &contentprovider.ModuleConfig{ + Name: "github.com/module-name", + Version: "0.0.1", + Channel: "regular", + Namespace: "kcp-system", + Manifest: "test", + Resources: contentprovider.ResourcesMap{ + "name": "", + }, + }, + expectedError: fmt.Errorf("failed to validate resources: %w: link must not be empty", commonerrors.ErrInvalidOption), }, } for _, test := range tests { @@ -379,6 +307,8 @@ var expectedReturnedModuleConfig = contentprovider.ModuleConfig{ Version: "v1alpha3", Kind: "Gateway", }, + Resources: contentprovider.ResourcesMap{ + "rawManifest": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", }, Manager: &contentprovider.Manager{ Name: "manager-name", @@ -395,16 +325,6 @@ func (*fileExistsStub) ReadFile(_ string) ([]byte, error) { return yaml.Marshal(expectedReturnedModuleConfig) } -type tmpfileSystemStub struct{} - -func (*tmpfileSystemStub) DownloadTempFile(_ string, _ string, _ *url.URL) (string, error) { - return "file.yaml", nil -} - -func (*tmpfileSystemStub) RemoveTempFiles() []error { - return nil -} - type fileDoesNotExistStub struct{} func (*fileDoesNotExistStub) FileExists(_ string) (bool, error) { diff --git a/internal/service/templategenerator/templategenerator.go b/internal/service/templategenerator/templategenerator.go index d8161f90..5860ca8e 100644 --- a/internal/service/templategenerator/templategenerator.go +++ b/internal/service/templategenerator/templategenerator.go @@ -85,20 +85,28 @@ spec: {{- end}} descriptor: {{yaml .Descriptor | printf "%s" | indent 4}} +{{- with .Resources}} + resources: + {{- range $key, $value := . }} + - name: {{ $key }} + link: {{ $value }} + {{- end}} +{{- end}} ` ) type moduleTemplateData struct { - ResourceName string - Namespace string - Descriptor compdesc.ComponentDescriptorVersion - Channel string - Labels map[string]string - Annotations map[string]string - Mandatory bool - Data string + ResourceName string + Namespace string + Descriptor compdesc.ComponentDescriptorVersion + Channel string + Labels map[string]string + Annotations map[string]string + Mandatory bool + Data string AssociatedResources []*metav1.GroupVersionKind - Manager *contentprovider.Manager + Resources contentprovider.ResourcesMap + Manager *contentprovider.Manager } func (s *Service) GenerateModuleTemplate( @@ -142,21 +150,28 @@ func (s *Service) GenerateModuleTemplate( } mtData := moduleTemplateData{ - ResourceName: moduleConfig.ResourceName, - Namespace: moduleConfig.Namespace, - Descriptor: cva, - Channel: moduleConfig.Channel, - Labels: labels, - Annotations: annotations, - Mandatory: moduleConfig.Mandatory, + ResourceName: moduleConfig.ResourceName, + Namespace: moduleConfig.Namespace, + Descriptor: cva, + Channel: moduleConfig.Channel, + Labels: labels, + Annotations: annotations, + Mandatory: moduleConfig.Mandatory, AssociatedResources: moduleConfig.AssociatedResources, - Manager: moduleConfig.Manager, + Resources: contentprovider.ResourcesMap{ + "rawManifest": moduleConfig.Manifest, // defaults rawManifest to Manifest; may be overwritten by explicitly provided entries + }, + Manager: moduleConfig.Manager, } if len(data) > 0 { mtData.Data = string(data) } + for name, link := range moduleConfig.Resources { + mtData.Resources[name] = link + } + w := &bytes.Buffer{} if err = moduleTemplate.Execute(w, mtData); err != nil { return fmt.Errorf("failed to execute template, %w", err) diff --git a/internal/service/templategenerator/templategenerator_test.go b/internal/service/templategenerator/templategenerator_test.go index 51a23722..04c6afa7 100644 --- a/internal/service/templategenerator/templategenerator_test.go +++ b/internal/service/templategenerator/templategenerator_test.go @@ -49,6 +49,8 @@ func TestGenerateModuleTemplate_Success(t *testing.T) { Labels: map[string]string{"key": "value"}, Annotations: map[string]string{"annotation": "value"}, Mandatory: true, + Manifest: "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + Resources: contentprovider.ResourcesMap{"someResource": "https://some.other/location/template-operator.yaml"}, } descriptor := testutils.CreateComponentDescriptor("example.com/component", "1.0.0") data := []byte("test-data") @@ -62,6 +64,30 @@ func TestGenerateModuleTemplate_Success(t *testing.T) { require.Contains(t, mockFS.writtenTemplate, "stable") require.Contains(t, mockFS.writtenTemplate, "test-data") require.Contains(t, mockFS.writtenTemplate, "example.com/component") + require.Contains(t, mockFS.writtenTemplate, "someResource") + require.Contains(t, mockFS.writtenTemplate, "https://some.other/location/template-operator.yaml") + require.Contains(t, mockFS.writtenTemplate, "rawManifest") + require.Contains(t, mockFS.writtenTemplate, "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml") +} + +func TestGenerateModuleTemplate_Success_With_Overwritten_RawManifest(t *testing.T) { + mockFS := &mockFileSystem{} + svc, _ := templategenerator.NewService(mockFS) + + moduleConfig := &contentprovider.ModuleConfig{ + Manifest: "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + Resources: contentprovider.ResourcesMap{"rawManifest": "https://some.other/location/template-operator.yaml"}, + } + descriptor := testutils.CreateComponentDescriptor("example.com/component", "1.0.0") + data := []byte("test-data") + + err := svc.GenerateModuleTemplate(moduleConfig, descriptor, data, true, "output.yaml") + + require.NoError(t, err) + require.Equal(t, "output.yaml", mockFS.path) + require.Contains(t, mockFS.writtenTemplate, "rawManifest") + require.Contains(t, mockFS.writtenTemplate, "https://some.other/location/template-operator.yaml") + require.NotContains(t, mockFS.writtenTemplate, "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml") } func TestGenerateModuleTemplateWithAssociatedResources_Success(t *testing.T) { diff --git a/tests/e2e/create/create_suite_test.go b/tests/e2e/create/create_suite_test.go index aa606a7a..9b169f89 100644 --- a/tests/e2e/create/create_suite_test.go +++ b/tests/e2e/create/create_suite_test.go @@ -21,20 +21,27 @@ const ( testdataDir = "./testdata/moduleconfig/" invalidConfigs = testdataDir + "invalid/" + duplicateResources = invalidConfigs + "duplicate-resources.yaml" + emptyResourceName = invalidConfigs + "empty-resource-name.yaml" missingNameConfig = invalidConfigs + "missing-name.yaml" missingChannelConfig = invalidConfigs + "missing-channel.yaml" missingVersionConfig = invalidConfigs + "missing-version.yaml" missingManifestConfig = invalidConfigs + "missing-manifest.yaml" - - validConfigs = testdataDir + "valid/" - minimalConfig = validConfigs + "minimal.yaml" - withAnnotationsConfig = validConfigs + "with-annotations.yaml" - withDefaultCrConfig = validConfigs + "with-defaultcr.yaml" - withSecurityConfig = validConfigs + "with-security.yaml" - withMandatoryConfig = validConfigs + "with-mandatory.yaml" + nonHttpsResource = invalidConfigs + "non-https-resource.yaml" + resourceWithoutLink = invalidConfigs + "resource-without-link.yaml" + resourceWithoutName = invalidConfigs + "resource-without-name.yaml" + + validConfigs = testdataDir + "valid/" + minimalConfig = validConfigs + "minimal.yaml" + withAnnotationsConfig = validConfigs + "with-annotations.yaml" + withDefaultCrConfig = validConfigs + "with-defaultcr.yaml" + withSecurityConfig = validConfigs + "with-security.yaml" + withMandatoryConfig = validConfigs + "with-mandatory.yaml" withAssociatedResourcesConfig = validConfigs + "with-associated-resources.yaml" - withManagerConfig = validConfigs + "with-manager.yaml" - withNoNamespaceManagerConfig = validConfigs + "with-manager-no-namespace.yaml" + withResources = validConfigs + "with-resources.yaml" + withResourcesOverwrite = validConfigs + "with-resources-overwrite.yaml" + withManagerConfig = validConfigs + "with-manager.yaml" + withNoNamespaceManagerConfig = validConfigs + "with-manager-no-namespace.yaml" ociRegistry = "http://k3d-oci.localhost:5001" templateOutputPath = "/tmp/template.yaml" diff --git a/tests/e2e/create/create_test.go b/tests/e2e/create/create_test.go index 6f4aeab5..77708571 100644 --- a/tests/e2e/create/create_test.go +++ b/tests/e2e/create/create_test.go @@ -39,7 +39,7 @@ var _ = Describe("Test 'create' command", Ordered, func() { Context("Given 'modulectl create' command", func() { var cmd createCmd - It("When invoked with '--module-config-file' using file with missing name", func() { + It("When invoked with '--config-file' using file with missing name", func() { cmd = createCmd{ moduleConfigFile: missingNameConfig, } @@ -53,7 +53,7 @@ var _ = Describe("Test 'create' command", Ordered, func() { Context("Given 'modulectl create' command", func() { var cmd createCmd - It("When invoked with '--module-config-file' using file with missing channel", func() { + It("When invoked with '--config-file' using file with missing channel", func() { cmd = createCmd{ moduleConfigFile: missingChannelConfig, } @@ -67,7 +67,7 @@ var _ = Describe("Test 'create' command", Ordered, func() { Context("Given 'modulectl create' command", func() { var cmd createCmd - It("When invoked with '--module-config-file' using file with missing version", func() { + It("When invoked with '--config-file' using file with missing version", func() { cmd = createCmd{ moduleConfigFile: missingVersionConfig, } @@ -81,7 +81,7 @@ var _ = Describe("Test 'create' command", Ordered, func() { Context("Given 'modulectl create' command", func() { var cmd createCmd - It("When invoked with '--module-config-file' using file with missing manifest", func() { + It("When invoked with '--config-file' using file with missing manifest", func() { cmd = createCmd{ moduleConfigFile: missingManifestConfig, } @@ -89,13 +89,83 @@ var _ = Describe("Test 'create' command", Ordered, func() { It("Then the command should fail", func() { err := cmd.execute() Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("failed to parse module config: failed to value module config: manifest path must not be empty: invalid Option")) + Expect(err.Error()).Should(ContainSubstring("failed to parse module config: failed to validate module config: manifest must not be empty: invalid Option")) }) }) Context("Given 'modulectl create' command", func() { var cmd createCmd - It("When invoked with '--module-config-file' using valid file", func() { + It("When invoked with duplicate entry in resources", func() { + cmd = createCmd{ + moduleConfigFile: duplicateResources, + } + }) + It("Then the command should fail", func() { + err := cmd.execute() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("failed to parse module config file: resources contain duplicate entries")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with empty resource name", func() { + cmd = createCmd{ + moduleConfigFile: emptyResourceName, + } + }) + It("Then the command should fail", func() { + err := cmd.execute() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("failed to parse module config: failed to validate module config: failed to validate resources: invalid Option: name must not be empty")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with non https resource", func() { + cmd = createCmd{ + moduleConfigFile: nonHttpsResource, + } + }) + It("Then the command should fail", func() { + err := cmd.execute() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("failed to parse module config: failed to validate module config: failed to validate resources: invalid Option: link http://some.other/location/template-operator.yaml is not using https scheme")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with invalid resource - link missing", func() { + cmd = createCmd{ + moduleConfigFile: resourceWithoutLink, + } + }) + It("Then the command should fail", func() { + err := cmd.execute() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("failed to parse module config: failed to validate module config: failed to validate resources: invalid Option: link must not be empty")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with invalid resource - name missing", func() { + cmd = createCmd{ + moduleConfigFile: resourceWithoutName, + } + }) + It("Then the command should fail", func() { + err := cmd.execute() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("failed to parse module config: failed to validate module config: failed to validate resources: invalid Option: name must not be empty")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with '--config-file' using valid file", func() { cmd = createCmd{ moduleConfigFile: minimalConfig, } @@ -188,6 +258,11 @@ var _ = Describe("Test 'create' command", Ordered, func() { By("And spec.manager should be nil") Expect(template.Spec.Manager).To(BeNil()) + + By("And spec.resources should contain rawManifest") + Expect(template.Spec.Resources).To(HaveLen(1)) + Expect(template.Spec.Resources[0].Name).To(Equal("rawManifest")) + Expect(template.Spec.Resources[0].Link).To(Equal("https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml")) }) }) @@ -467,13 +542,14 @@ var _ = Describe("Test 'create' command", Ordered, func() { Expect(manager.Kind).To(Equal("Deployment")) }) }) - + + Context("Given 'modulectl create' command", func() { var cmd createCmd It("When invoked with valid module-config containing associatedResources list", func() { cmd = createCmd{ moduleConfigFile: withAssociatedResourcesConfig, - registry: ociRegistry, + registry: ociRegistry, insecure: true, output: templateOutputPath, } @@ -503,6 +579,60 @@ var _ = Describe("Test 'create' command", Ordered, func() { Expect(resources[0].Kind).To(Equal("Gateway")) }) }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with minimal valid module-config containing resources", func() { + cmd = createCmd{ + moduleConfigFile: withResources, + registry: ociRegistry, + insecure: true, + output: templateOutputPath, + } + }) + It("Then the command should succeed", func() { + Expect(cmd.execute()).To(Succeed()) + + By("And module template file should be generated") + Expect(filesIn("/tmp/")).Should(ContainElement("template.yaml")) + }) + It("Then module template should contain merged .spec.resources", func() { + template, err := readModuleTemplate(templateOutputPath) + Expect(err).ToNot(HaveOccurred()) + + Expect(template.Spec.Resources).To(HaveLen(2)) + Expect(template.Spec.Resources[0].Name).To(Equal("rawManifest")) + Expect(template.Spec.Resources[0].Link).To(Equal("https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml")) + Expect(template.Spec.Resources[1].Name).To(Equal("someResource")) + Expect(template.Spec.Resources[1].Link).To(Equal("https://some.other/location/template-operator.yaml")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with minimal valid module-config containing rawManfiest in resources", func() { + cmd = createCmd{ + moduleConfigFile: withResourcesOverwrite, + registry: ociRegistry, + insecure: true, + output: templateOutputPath, + } + }) + It("Then the command should succeed", func() { + Expect(cmd.execute()).To(Succeed()) + + By("And module template file should be generated") + Expect(filesIn("/tmp/")).Should(ContainElement("template.yaml")) + }) + It("Then module template should contain rawManifest value from module-config", func() { + template, err := readModuleTemplate(templateOutputPath) + Expect(err).ToNot(HaveOccurred()) + + Expect(template.Spec.Resources).To(HaveLen(1)) + Expect(template.Spec.Resources[0].Name).To(Equal("rawManifest")) + Expect(template.Spec.Resources[0].Link).To(Equal("https://some.other/location/template-operator.yaml")) + }) + }) }) // Test helper functions diff --git a/tests/e2e/create/testdata/moduleconfig/invalid/duplicate-resources.yaml b/tests/e2e/create/testdata/moduleconfig/invalid/duplicate-resources.yaml new file mode 100644 index 00000000..3cc800e8 --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/invalid/duplicate-resources.yaml @@ -0,0 +1,9 @@ +name: kyma-project.io/module/template-operator +channel: regular +version: 1.0.0 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml +resources: +- name: someResource + link: https://some.other/location/template-operator.yaml +- name: someResource + link: https://some.other/location/template-operator.yaml diff --git a/tests/e2e/create/testdata/moduleconfig/invalid/empty-resource-name.yaml b/tests/e2e/create/testdata/moduleconfig/invalid/empty-resource-name.yaml new file mode 100644 index 00000000..ac32b8d3 --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/invalid/empty-resource-name.yaml @@ -0,0 +1,7 @@ +name: kyma-project.io/module/template-operator +channel: regular +version: 1.0.0 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml +resources: +- name: "" + link: http://some.other/location/template-operator.yaml diff --git a/tests/e2e/create/testdata/moduleconfig/invalid/non-https-resource.yaml b/tests/e2e/create/testdata/moduleconfig/invalid/non-https-resource.yaml new file mode 100644 index 00000000..5257b8aa --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/invalid/non-https-resource.yaml @@ -0,0 +1,7 @@ +name: kyma-project.io/module/template-operator +channel: regular +version: 1.0.0 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml +resources: +- name: someResource + link: http://some.other/location/template-operator.yaml diff --git a/tests/e2e/create/testdata/moduleconfig/invalid/resource-without-link.yaml b/tests/e2e/create/testdata/moduleconfig/invalid/resource-without-link.yaml new file mode 100644 index 00000000..564d4e72 --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/invalid/resource-without-link.yaml @@ -0,0 +1,6 @@ +name: kyma-project.io/module/template-operator +channel: regular +version: 1.0.0 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml +resources: +- name: someResource diff --git a/tests/e2e/create/testdata/moduleconfig/invalid/resource-without-name.yaml b/tests/e2e/create/testdata/moduleconfig/invalid/resource-without-name.yaml new file mode 100644 index 00000000..7444f577 --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/invalid/resource-without-name.yaml @@ -0,0 +1,6 @@ +name: kyma-project.io/module/template-operator +channel: regular +version: 1.0.0 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml +resources: +- link: https://some.other/location/template-operator.yaml diff --git a/tests/e2e/create/testdata/moduleconfig/valid/with-resources-overwrite.yaml b/tests/e2e/create/testdata/moduleconfig/valid/with-resources-overwrite.yaml new file mode 100644 index 00000000..225f8247 --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/valid/with-resources-overwrite.yaml @@ -0,0 +1,7 @@ +name: kyma-project.io/module/template-operator +channel: regular +version: 1.0.8 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml +resources: +- name: rawManifest + link: https://some.other/location/template-operator.yaml diff --git a/tests/e2e/create/testdata/moduleconfig/valid/with-resources.yaml b/tests/e2e/create/testdata/moduleconfig/valid/with-resources.yaml new file mode 100644 index 00000000..d455f767 --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/valid/with-resources.yaml @@ -0,0 +1,7 @@ +name: kyma-project.io/module/template-operator +channel: regular +version: 1.0.9 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml +resources: +- name: someResource + link: https://some.other/location/template-operator.yaml diff --git a/unit-test-coverage.yaml b/unit-test-coverage.yaml index 80773ba3..e791aeab 100644 --- a/unit-test-coverage.yaml +++ b/unit-test-coverage.yaml @@ -2,13 +2,14 @@ packages: cmd/modulectl/scaffold: 100 internal/common/validation: 86 internal/service/scaffold: 92 - internal/service/contentprovider: 100 + internal/service/contentprovider: 98 internal/service/filegenerator: 100 + internal/service/fileresolver: 92 internal/service/moduleconfig/generator: 100 - internal/service/moduleconfig/reader: 45 - internal/service/create: 34 + internal/service/moduleconfig/reader: 50 + internal/service/create: 43 internal/service/componentdescriptor: 78 internal/service/templategenerator: 78 - internal/service/crdparser: 71 - internal/service/registry: 51 + internal/service/crdparser: 73 + internal/service/registry: 52 internal/service/componentarchive: 37