From 18c28f50141cc65c24287b3fccf74bd9e2eafbf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Wed, 16 Oct 2024 08:57:51 +0200 Subject: [PATCH 01/18] feat: Generate '.spec.resources' --- tests/e2e/create/create_suite_test.go | 14 +++-- tests/e2e/create/create_test.go | 55 ++++++++++++++++++- .../valid/with-resources-overwrite.yaml | 7 +++ .../moduleconfig/valid/with-resources.yaml | 7 +++ 4 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 tests/e2e/create/testdata/moduleconfig/valid/with-resources-overwrite.yaml create mode 100644 tests/e2e/create/testdata/moduleconfig/valid/with-resources.yaml diff --git a/tests/e2e/create/create_suite_test.go b/tests/e2e/create/create_suite_test.go index 48a67953..76df9cd9 100644 --- a/tests/e2e/create/create_suite_test.go +++ b/tests/e2e/create/create_suite_test.go @@ -26,12 +26,14 @@ const ( 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" + 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" + withResources = validConfigs + "with-resources.yaml" + withResourcesOverwrite = validConfigs + "with-resources-overwrite.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 d1d3c580..45df9f12 100644 --- a/tests/e2e/create/create_test.go +++ b/tests/e2e/create/create_test.go @@ -10,7 +10,7 @@ import ( "ocm.software/ocm/api/ocm" "ocm.software/ocm/api/ocm/compdesc" ocmv1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" - "ocm.software/ocm/api/ocm/compdesc/versions/v2" + v2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" "ocm.software/ocm/api/ocm/extensions/accessmethods/github" "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob" "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" @@ -182,6 +182,11 @@ var _ = Describe("Test 'create' command", Ordered, func() { By("And spec.mandatory should be false") Expect(template.Spec.Mandatory).To(BeFalse()) + + 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")) }) }) @@ -388,6 +393,54 @@ var _ = Describe("Test 'create' command", Ordered, func() { }) }) +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() { + Expect(template.Spec.Resources).To(HaveLen(2)) + Expect(template.Spec.Resources[0].Name).To(Equal("notRawManifest")) + Expect(template.Spec.Resources[0].Link).To(Equal("https://some.other/location/template-operator.yaml")) + Expect(template.Spec.Resources[1].Name).To(Equal("rawManifest")) + Expect(template.Spec.Resources[1].Link).To(Equal("https://github.com/kyma-project/template-operator/releases/download/1.0.1/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: 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 rawManifest value from module-config", func() { + 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 func readModuleTemplate(filepath string) (*v1beta2.ModuleTemplate, error) { 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..8de9607a --- /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.0 +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..997ce429 --- /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.0 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml +resources: +- name: notRawManifest + link: https://some.other/location/template-operator.yaml From f799f35b66e4d6431ac5f5225caf6de21586b4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Wed, 16 Oct 2024 16:26:43 +0200 Subject: [PATCH 02/18] implement --- internal/common/validation/validation.go | 14 ++ internal/common/validation/validation_test.go | 48 +++++++ .../service/contentprovider/moduleconfig.go | 34 +++++ .../reader/moduleconfig_reader.go | 12 +- .../reader/moduleconfig_reader_test.go | 20 +++ .../templategenerator/templategenerator.go | 14 ++ .../templategenerator_test.go | 26 ++++ tests/e2e/create/create_suite_test.go | 3 + tests/e2e/create/create_test.go | 134 ++++++++++++------ .../invalid/duplicate-resources.yaml | 9 ++ .../invalid/empty-resource-name.yaml | 7 + .../invalid/non-https-resource.yaml | 7 + .../moduleconfig/valid/with-resources.yaml | 2 +- 13 files changed, 285 insertions(+), 45 deletions(-) create mode 100644 tests/e2e/create/testdata/moduleconfig/invalid/duplicate-resources.yaml create mode 100644 tests/e2e/create/testdata/moduleconfig/invalid/empty-resource-name.yaml create mode 100644 tests/e2e/create/testdata/moduleconfig/invalid/non-https-resource.yaml diff --git a/internal/common/validation/validation.go b/internal/common/validation/validation.go index 7c0096c8..8a582254 100644 --- a/internal/common/validation/validation.go +++ b/internal/common/validation/validation.go @@ -2,6 +2,7 @@ package validation import ( "fmt" + "net/url" "regexp" "strings" @@ -99,6 +100,19 @@ func ValidateModuleNamespace(namespace string) error { return nil } +func ValidateIsValidHttpsUrl(input string) error { + _url, err := url.Parse(input) + if err != nil { + return fmt.Errorf("%w: %s is not a valid URL", commonerrors.ErrInvalidOption, input) + } + + if _url.Scheme != "https" { + return fmt.Errorf("%w: %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 b00bbb43..a6904da2 100644 --- a/internal/common/validation/validation_test.go +++ b/internal/common/validation/validation_test.go @@ -1,6 +1,7 @@ package validation_test import ( + "fmt" "testing" "github.com/kyma-project/modulectl/internal/common/validation" @@ -179,3 +180,50 @@ func TestValidateModuleNamespace(t *testing.T) { }) } } + +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 2f23ae33..7efa1fb2 100644 --- a/internal/service/contentprovider/moduleconfig.go +++ b/internal/service/contentprovider/moduleconfig.go @@ -82,4 +82,38 @@ type ModuleConfig struct { Beta bool `yaml:"beta" comment:"optional, default=false, determines whether the ModuleTemplate should have the beta flag or not"` Labels map[string]string `yaml:"labels" comment:"optional, additional labels for the ModuleTemplate"` Annotations map[string]string `yaml:"annotations" comment:"optional, additional annotations for the ModuleTemplate"` + Resources ResourcesMap `yaml:"resources" 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 { + var 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 fmt.Errorf("list contains duplicate entries") + } + + return nil +} + +func (rm ResourcesMap) MarshalYAML() (interface{}, error) { + var resources []resource + for name, link := range rm { + resources = append(resources, resource{Name: name, Link: link}) + } + return resources, nil } diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader.go b/internal/service/moduleconfig/reader/moduleconfig_reader.go index af33d0b6..8030f064 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader.go @@ -58,7 +58,7 @@ func (s *Service) ParseAndValidateModuleConfig(moduleConfigFile string, } if err = ValidateModuleConfig(moduleConfig); err != nil { - return nil, fmt.Errorf("failed to value module config: %w", err) + return nil, fmt.Errorf("failed to validate module config: %w", err) } moduleConfig.DefaultCRPath, err = GetDefaultCRPath(moduleConfig.DefaultCRPath, s.tempFileSystem) @@ -144,6 +144,16 @@ func ValidateModuleConfig(moduleConfig *contentprovider.ModuleConfig) error { return fmt.Errorf("failed to validate module namespace: %w", err) } + for name, link := range moduleConfig.Resources { + if name == "" { + return fmt.Errorf("failed to validate resource name: %w: name must not be empty", commonerrors.ErrInvalidOption) + } + + if err := validation.ValidateIsValidHttpsUrl(link); err != nil { + return fmt.Errorf("failed to validate resource link: %w", err) + } + } + if moduleConfig.ManifestPath == "" { return fmt.Errorf("manifest path must not be empty: %w", commonerrors.ErrInvalidOption) } diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go index 55225ef6..35475f75 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go @@ -42,6 +42,9 @@ func Test_ParseModuleConfig_Returns_CorrectModuleConfig(t *testing.T) { require.False(t, result.Beta) require.Equal(t, map[string]string{"label1": "value1"}, result.Labels) require.Equal(t, map[string]string{"annotation1": "value1"}, result.Annotations) + require.Equal(t, contentprovider.ResourcesMap{ + "rawManifest": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + }, result.Resources) } func TestNew_CalledWithNilDependencies_ReturnsErr(t *testing.T) { @@ -231,6 +234,20 @@ func Test_ValidateModuleConfig(t *testing.T) { }, expectedError: fmt.Errorf("manifest path must not be empty: %w", commonerrors.ErrInvalidOption), }, + { + name: "invalid module resources - duplicate key", + moduleConfig: &contentprovider.ModuleConfig{ + Name: "github.com/module-name", + Version: "0.0.1", + Channel: "regular", + Namespace: "kcp-system", + ManifestPath: "test", + Resources: contentprovider.ResourcesMap{ + "key": "%% not a URL", + }, + }, + expectedError: fmt.Errorf("failed to validate resource link: invalid Option: %%%% not a URL is not a valid URL"), + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -266,6 +283,9 @@ var expectedReturnedModuleConfig = contentprovider.ModuleConfig{ Beta: false, Labels: map[string]string{"label1": "value1"}, Annotations: map[string]string{"annotation1": "value1"}, + Resources: contentprovider.ResourcesMap{ + "rawManifest": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + }, } func (*fileExistsStub) ReadFile(_ string) ([]byte, error) { diff --git a/internal/service/templategenerator/templategenerator.go b/internal/service/templategenerator/templategenerator.go index 1c69e82c..8e1e8cc0 100644 --- a/internal/service/templategenerator/templategenerator.go +++ b/internal/service/templategenerator/templategenerator.go @@ -66,6 +66,12 @@ spec: {{- end}} descriptor: {{yaml .Descriptor | printf "%s" | indent 4}} +{{- with .Resources}} + resources: + {{- range $key, $value := . }} + {{ printf "%q" $key }}: {{ printf "%q" $value }} + {{- end}} +{{- end}} ` ) @@ -78,6 +84,7 @@ type moduleTemplateData struct { Annotations map[string]string Mandatory bool Data string + Resources contentprovider.ResourcesMap } func (s *Service) GenerateModuleTemplate( @@ -128,12 +135,19 @@ func (s *Service) GenerateModuleTemplate( Labels: labels, Annotations: annotations, Mandatory: moduleConfig.Mandatory, + Resources: contentprovider.ResourcesMap{ + "rawManifest": moduleConfig.ManifestPath, // defaults rawManifest to ManifestPath; may be overwritten by explicitly provided entries + }, } 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 510605b2..f350f919 100644 --- a/internal/service/templategenerator/templategenerator_test.go +++ b/internal/service/templategenerator/templategenerator_test.go @@ -47,6 +47,8 @@ func TestGenerateModuleTemplate_Success(t *testing.T) { Labels: map[string]string{"key": "value"}, Annotations: map[string]string{"annotation": "value"}, Mandatory: true, + ManifestPath: "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") @@ -60,6 +62,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{ + ManifestPath: "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") } type mockFileSystem struct { diff --git a/tests/e2e/create/create_suite_test.go b/tests/e2e/create/create_suite_test.go index 76df9cd9..5feffce8 100644 --- a/tests/e2e/create/create_suite_test.go +++ b/tests/e2e/create/create_suite_test.go @@ -21,10 +21,13 @@ 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" + nonHttpsResource = invalidConfigs + "non-https-resource.yaml" validConfigs = testdataDir + "valid/" minimalConfig = validConfigs + "minimal.yaml" diff --git a/tests/e2e/create/create_test.go b/tests/e2e/create/create_test.go index 45df9f12..e4741714 100644 --- a/tests/e2e/create/create_test.go +++ b/tests/e2e/create/create_test.go @@ -89,7 +89,49 @@ 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 path must not be empty: invalid Option")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + 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: list contains 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 resource name: 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 validate resource link: invalid Option: http://some.other/location/template-operator.yaml is not using https scheme")) }) }) @@ -391,53 +433,59 @@ var _ = Describe("Test 'create' command", Ordered, func() { Expect(template.Spec.Mandatory).To(BeTrue()) }) }) -}) -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()) + 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() { - Expect(template.Spec.Resources).To(HaveLen(2)) - Expect(template.Spec.Resources[0].Name).To(Equal("notRawManifest")) - Expect(template.Spec.Resources[0].Link).To(Equal("https://some.other/location/template-operator.yaml")) - Expect(template.Spec.Resources[1].Name).To(Equal("rawManifest")) - Expect(template.Spec.Resources[1].Link).To(Equal("https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml")) - }) -}) + 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()) -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: withResources, - registry: ociRegistry, - insecure: true, - output: templateOutputPath, - } + Expect(template.Spec.Resources).To(HaveLen(2)) + Expect(template.Spec.Resources[0].Name).To(Equal("someResource")) + Expect(template.Spec.Resources[0].Link).To(Equal("https://some.other/location/template-operator.yaml")) + Expect(template.Spec.Resources[1].Name).To(Equal("rawManifest")) + Expect(template.Spec.Resources[1].Link).To(Equal("https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml")) + }) }) - 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() { - 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")) + 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: 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 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")) + }) }) }) 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/valid/with-resources.yaml b/tests/e2e/create/testdata/moduleconfig/valid/with-resources.yaml index 997ce429..fc5fc9e6 100644 --- a/tests/e2e/create/testdata/moduleconfig/valid/with-resources.yaml +++ b/tests/e2e/create/testdata/moduleconfig/valid/with-resources.yaml @@ -3,5 +3,5 @@ channel: regular version: 1.0.0 manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml resources: -- name: notRawManifest +- name: someResource link: https://some.other/location/template-operator.yaml From bcd06f577f05d953295b9121259f4775afe5242c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Thu, 17 Oct 2024 08:45:29 +0200 Subject: [PATCH 03/18] omitempty + correct generation format --- internal/service/contentprovider/moduleconfig.go | 2 +- internal/service/templategenerator/templategenerator.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/service/contentprovider/moduleconfig.go b/internal/service/contentprovider/moduleconfig.go index 7efa1fb2..5cb89e9d 100644 --- a/internal/service/contentprovider/moduleconfig.go +++ b/internal/service/contentprovider/moduleconfig.go @@ -82,7 +82,7 @@ type ModuleConfig struct { Beta bool `yaml:"beta" comment:"optional, default=false, determines whether the ModuleTemplate should have the beta flag or not"` Labels map[string]string `yaml:"labels" comment:"optional, additional labels for the ModuleTemplate"` Annotations map[string]string `yaml:"annotations" comment:"optional, additional annotations for the ModuleTemplate"` - Resources ResourcesMap `yaml:"resources" comment:"optional, additional resources of the ModuleTemplate that may be fetched"` + Resources ResourcesMap `yaml:"resources,omitempty" comment:"optional, additional resources of the ModuleTemplate that may be fetched"` } type resource struct { diff --git a/internal/service/templategenerator/templategenerator.go b/internal/service/templategenerator/templategenerator.go index 8e1e8cc0..ab02b651 100644 --- a/internal/service/templategenerator/templategenerator.go +++ b/internal/service/templategenerator/templategenerator.go @@ -68,8 +68,9 @@ spec: {{yaml .Descriptor | printf "%s" | indent 4}} {{- with .Resources}} resources: - {{- range $key, $value := . }} - {{ printf "%q" $key }}: {{ printf "%q" $value }} + {{- range $key, $value := . }} + - name: {{ $key }} + link: {{ $value }} {{- end}} {{- end}} ` From 4b05af4a10f41f9ef1a55b613cef00ab21aa35b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Thu, 17 Oct 2024 09:15:37 +0200 Subject: [PATCH 04/18] split Manifest and ManifestFilePath --- .../service/contentprovider/moduleconfig.go | 31 ++++----- internal/service/create/create.go | 4 +- .../generator/moduleconfig_generator_test.go | 2 +- .../reader/moduleconfig_reader.go | 6 +- .../reader/moduleconfig_reader_test.go | 66 +++++++++---------- .../templategenerator/templategenerator.go | 2 +- .../templategenerator_test.go | 6 +- tests/e2e/create/create_test.go | 2 +- 8 files changed, 60 insertions(+), 59 deletions(-) diff --git a/internal/service/contentprovider/moduleconfig.go b/internal/service/contentprovider/moduleconfig.go index 5cb89e9d..ff7db6c3 100644 --- a/internal/service/contentprovider/moduleconfig.go +++ b/internal/service/contentprovider/moduleconfig.go @@ -36,7 +36,7 @@ func (s *ModuleConfigProvider) getModuleConfig(args types.KeyValueArgs) ModuleCo Name: args[ArgModuleName], Version: args[ArgModuleVersion], Channel: args[ArgModuleChannel], - ManifestPath: args[ArgManifestFile], + Manifest: args[ArgManifestFile], Security: args[ArgSecurityConfigFile], DefaultCRPath: args[ArgDefaultCRFile], } @@ -69,20 +69,21 @@ func (s *ModuleConfigProvider) validateArgs(args types.KeyValueArgs) error { } type ModuleConfig struct { - Name string `yaml:"name" comment:"required, the name of the Module"` - Version string `yaml:"version" comment:"required, the version of the Module"` - Channel string `yaml:"channel" comment:"required, channel that should be used in the ModuleTemplate"` - ManifestPath string `yaml:"manifest" comment:"required, relative path or remote URL to the manifests"` - Mandatory bool `yaml:"mandatory" comment:"optional, default=false, indicates whether the module is mandatory to be installed on all clusters"` - DefaultCRPath string `yaml:"defaultCR" comment:"optional, relative path or remote URL to a YAML file containing the default CR for the module"` - ResourceName string `yaml:"resourceName" comment:"optional, default={name}-{channel}, when channel is 'none', the default is {name}-{version}, the name for the ModuleTemplate that will be created"` - Namespace string `yaml:"namespace" comment:"optional, default=kcp-system, the namespace where the ModuleTemplate will be deployed"` - Security string `yaml:"security" comment:"optional, name of the security scanners config file"` - Internal bool `yaml:"internal" comment:"optional, default=false, determines whether the ModuleTemplate should have the internal flag or not"` - Beta bool `yaml:"beta" comment:"optional, default=false, determines whether the ModuleTemplate should have the beta flag or not"` - Labels map[string]string `yaml:"labels" comment:"optional, additional labels for the ModuleTemplate"` - Annotations map[string]string `yaml:"annotations" comment:"optional, additional annotations for the ModuleTemplate"` - Resources ResourcesMap `yaml:"resources,omitempty" comment:"optional, additional resources of the ModuleTemplate that may be fetched"` + Name string `yaml:"name" comment:"required, the name of the Module"` + Version string `yaml:"version" comment:"required, the version of the Module"` + Channel string `yaml:"channel" comment:"required, channel that should be used in the ModuleTemplate"` + Manifest string `yaml:"manifest" comment:"required, relative path or remote URL to the manifests"` + ManifestFilePath string `yaml:"-"` // ignore this field, will be filled programmatically + Mandatory bool `yaml:"mandatory" comment:"optional, default=false, indicates whether the module is mandatory to be installed on all clusters"` + DefaultCRPath string `yaml:"defaultCR" comment:"optional, relative path or remote URL to a YAML file containing the default CR for the module"` + ResourceName string `yaml:"resourceName" comment:"optional, default={name}-{channel}, when channel is 'none', the default is {name}-{version}, the name for the ModuleTemplate that will be created"` + Namespace string `yaml:"namespace" comment:"optional, default=kcp-system, the namespace where the ModuleTemplate will be deployed"` + Security string `yaml:"security" comment:"optional, name of the security scanners config file"` + Internal bool `yaml:"internal" comment:"optional, default=false, determines whether the ModuleTemplate should have the internal flag or not"` + Beta bool `yaml:"beta" comment:"optional, default=false, determines whether the ModuleTemplate should have the beta flag or not"` + Labels map[string]string `yaml:"labels" comment:"optional, additional labels for the ModuleTemplate"` + Annotations map[string]string `yaml:"annotations" comment:"optional, additional annotations for the ModuleTemplate"` + Resources ResourcesMap `yaml:"resources,omitempty" comment:"optional, additional resources of the ModuleTemplate that may be fetched"` } type resource struct { diff --git a/internal/service/create/create.go b/internal/service/create/create.go index bbefb392..7381fe49 100644 --- a/internal/service/create/create.go +++ b/internal/service/create/create.go @@ -132,7 +132,7 @@ func (s *Service) Run(opts Options) error { return fmt.Errorf("failed to populate component descriptor metadata: %w", err) } - moduleResources, err := componentdescriptor.GenerateModuleResources(moduleConfig.Version, moduleConfig.ManifestPath, + moduleResources, err := componentdescriptor.GenerateModuleResources(moduleConfig.Version, moduleConfig.ManifestFilePath, moduleConfig.DefaultCRPath, opts.RegistryCredSelector) if err != nil { return fmt.Errorf("failed to generate module resources: %w", err) @@ -169,7 +169,7 @@ func (s *Service) Run(opts Options) error { func (s *Service) pushImgAndCreateTemplate(archive *comparch.ComponentArchive, moduleConfig *contentprovider.ModuleConfig, opts Options) error { opts.Out.Write("- Pushing component version\n") - isCRDClusterScoped, err := s.crdParserService.IsCRDClusterScoped(moduleConfig.DefaultCRPath, moduleConfig.ManifestPath) + isCRDClusterScoped, err := s.crdParserService.IsCRDClusterScoped(moduleConfig.DefaultCRPath, moduleConfig.ManifestFilePath) if err != nil { return fmt.Errorf("failed to determine if CRD is cluster scoped: %w", err) } diff --git a/internal/service/moduleconfig/generator/moduleconfig_generator_test.go b/internal/service/moduleconfig/generator/moduleconfig_generator_test.go index 7150f442..515a6fe2 100644 --- a/internal/service/moduleconfig/generator/moduleconfig_generator_test.go +++ b/internal/service/moduleconfig/generator/moduleconfig_generator_test.go @@ -120,7 +120,7 @@ func (*fileExistsStub) ReadFile(_ string) ([]byte, error) { Name: "module-name", Version: "0.0.1", Channel: "regular", - ManifestPath: "path/to/manifests", + Manifest: "path/to/manifests", Mandatory: false, DefaultCRPath: "path/to/defaultCR", ResourceName: "module-name-0.0.1", diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader.go b/internal/service/moduleconfig/reader/moduleconfig_reader.go index 8030f064..a69fc162 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader.go @@ -66,7 +66,7 @@ func (s *Service) ParseAndValidateModuleConfig(moduleConfigFile string, return nil, fmt.Errorf("failed to get default CR path: %w", err) } - moduleConfig.ManifestPath, err = GetManifestPath(moduleConfig.ManifestPath, s.tempFileSystem) + moduleConfig.ManifestFilePath, err = GetManifestPath(moduleConfig.Manifest, s.tempFileSystem) if err != nil { return nil, fmt.Errorf("failed to get manifest path: %w", err) } @@ -154,8 +154,8 @@ func ValidateModuleConfig(moduleConfig *contentprovider.ModuleConfig) error { } } - if moduleConfig.ManifestPath == "" { - return fmt.Errorf("manifest path must not be empty: %w", commonerrors.ErrInvalidOption) + if moduleConfig.Manifest == "" { + return fmt.Errorf("manifest must not be empty: %w", commonerrors.ErrInvalidOption) } return nil diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go index 35475f75..458843cd 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go @@ -32,7 +32,7 @@ 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/manifests", result.Manifest) require.Equal(t, "path/to/defaultCR", result.DefaultCRPath) require.Equal(t, "module-name-0.0.1", result.ResourceName) require.False(t, result.Mandatory) @@ -182,66 +182,66 @@ 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 - duplicate key", moduleConfig: &contentprovider.ModuleConfig{ - Name: "github.com/module-name", - Version: "0.0.1", - Channel: "regular", - Namespace: "kcp-system", - ManifestPath: "test", + Name: "github.com/module-name", + Version: "0.0.1", + Channel: "regular", + Namespace: "kcp-system", + Manifest: "test", Resources: contentprovider.ResourcesMap{ "key": "%% not a URL", }, @@ -273,7 +273,7 @@ var expectedReturnedModuleConfig = contentprovider.ModuleConfig{ Name: "github.com/module-name", Version: "0.0.1", Channel: "regular", - ManifestPath: "path/to/manifests", + Manifest: "path/to/manifests", Mandatory: false, DefaultCRPath: "path/to/defaultCR", ResourceName: "module-name-0.0.1", diff --git a/internal/service/templategenerator/templategenerator.go b/internal/service/templategenerator/templategenerator.go index ab02b651..83b31413 100644 --- a/internal/service/templategenerator/templategenerator.go +++ b/internal/service/templategenerator/templategenerator.go @@ -137,7 +137,7 @@ func (s *Service) GenerateModuleTemplate( Annotations: annotations, Mandatory: moduleConfig.Mandatory, Resources: contentprovider.ResourcesMap{ - "rawManifest": moduleConfig.ManifestPath, // defaults rawManifest to ManifestPath; may be overwritten by explicitly provided entries + "rawManifest": moduleConfig.Manifest, // defaults rawManifest to Manifest; may be overwritten by explicitly provided entries }, } diff --git a/internal/service/templategenerator/templategenerator_test.go b/internal/service/templategenerator/templategenerator_test.go index f350f919..0c94f70c 100644 --- a/internal/service/templategenerator/templategenerator_test.go +++ b/internal/service/templategenerator/templategenerator_test.go @@ -47,7 +47,7 @@ func TestGenerateModuleTemplate_Success(t *testing.T) { Labels: map[string]string{"key": "value"}, Annotations: map[string]string{"annotation": "value"}, Mandatory: true, - ManifestPath: "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + 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") @@ -73,8 +73,8 @@ func TestGenerateModuleTemplate_Success_With_Overwritten_RawManifest(t *testing. svc, _ := templategenerator.NewService(mockFS) moduleConfig := &contentprovider.ModuleConfig{ - ManifestPath: "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"}, + 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") diff --git a/tests/e2e/create/create_test.go b/tests/e2e/create/create_test.go index e4741714..2f2e64b3 100644 --- a/tests/e2e/create/create_test.go +++ b/tests/e2e/create/create_test.go @@ -89,7 +89,7 @@ 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 validate 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")) }) }) From 4087df89503f88026640fc79559cc8ad3e3beceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Thu, 17 Oct 2024 09:22:38 +0200 Subject: [PATCH 05/18] fix tests --- tests/e2e/create/create_test.go | 10 +++++----- .../moduleconfig/valid/with-resources-overwrite.yaml | 2 +- .../testdata/moduleconfig/valid/with-resources.yaml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/e2e/create/create_test.go b/tests/e2e/create/create_test.go index 2f2e64b3..36525d9b 100644 --- a/tests/e2e/create/create_test.go +++ b/tests/e2e/create/create_test.go @@ -455,10 +455,10 @@ var _ = Describe("Test 'create' command", Ordered, func() { Expect(err).ToNot(HaveOccurred()) Expect(template.Spec.Resources).To(HaveLen(2)) - Expect(template.Spec.Resources[0].Name).To(Equal("someResource")) - Expect(template.Spec.Resources[0].Link).To(Equal("https://some.other/location/template-operator.yaml")) - Expect(template.Spec.Resources[1].Name).To(Equal("rawManifest")) - Expect(template.Spec.Resources[1].Link).To(Equal("https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml")) + 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")) }) }) @@ -466,7 +466,7 @@ var _ = Describe("Test 'create' command", Ordered, func() { var cmd createCmd It("When invoked with minimal valid module-config containing rawManfiest in resources", func() { cmd = createCmd{ - moduleConfigFile: withResources, + moduleConfigFile: withResourcesOverwrite, registry: ociRegistry, insecure: true, output: templateOutputPath, diff --git a/tests/e2e/create/testdata/moduleconfig/valid/with-resources-overwrite.yaml b/tests/e2e/create/testdata/moduleconfig/valid/with-resources-overwrite.yaml index 8de9607a..5898f9de 100644 --- a/tests/e2e/create/testdata/moduleconfig/valid/with-resources-overwrite.yaml +++ b/tests/e2e/create/testdata/moduleconfig/valid/with-resources-overwrite.yaml @@ -1,6 +1,6 @@ name: kyma-project.io/module/template-operator channel: regular -version: 1.0.0 +version: 1.0.5 manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml resources: - name: rawManifest diff --git a/tests/e2e/create/testdata/moduleconfig/valid/with-resources.yaml b/tests/e2e/create/testdata/moduleconfig/valid/with-resources.yaml index fc5fc9e6..5959b565 100644 --- a/tests/e2e/create/testdata/moduleconfig/valid/with-resources.yaml +++ b/tests/e2e/create/testdata/moduleconfig/valid/with-resources.yaml @@ -1,6 +1,6 @@ name: kyma-project.io/module/template-operator channel: regular -version: 1.0.0 +version: 1.0.6 manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml resources: - name: someResource From 78e2e0e82b7b1460071b2c9822a0f7314120c4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Thu, 17 Oct 2024 09:50:58 +0200 Subject: [PATCH 06/18] extend e2e tests --- .../reader/moduleconfig_reader.go | 4 +++ tests/e2e/create/create_suite_test.go | 2 ++ tests/e2e/create/create_test.go | 28 +++++++++++++++++++ .../invalid/resource-without-link.yaml | 6 ++++ .../invalid/resource-without-name.yaml | 6 ++++ 5 files changed, 46 insertions(+) create mode 100644 tests/e2e/create/testdata/moduleconfig/invalid/resource-without-link.yaml create mode 100644 tests/e2e/create/testdata/moduleconfig/invalid/resource-without-name.yaml diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader.go b/internal/service/moduleconfig/reader/moduleconfig_reader.go index a69fc162..18ab9be1 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader.go @@ -149,6 +149,10 @@ func ValidateModuleConfig(moduleConfig *contentprovider.ModuleConfig) error { return fmt.Errorf("failed to validate resource name: %w: name must not be empty", commonerrors.ErrInvalidOption) } + if link == "" { + return fmt.Errorf("failed to validate resource link: %w: link must not be empty", commonerrors.ErrInvalidOption) + } + if err := validation.ValidateIsValidHttpsUrl(link); err != nil { return fmt.Errorf("failed to validate resource link: %w", err) } diff --git a/tests/e2e/create/create_suite_test.go b/tests/e2e/create/create_suite_test.go index 5feffce8..5b6dc750 100644 --- a/tests/e2e/create/create_suite_test.go +++ b/tests/e2e/create/create_suite_test.go @@ -28,6 +28,8 @@ const ( missingVersionConfig = invalidConfigs + "missing-version.yaml" missingManifestConfig = invalidConfigs + "missing-manifest.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" diff --git a/tests/e2e/create/create_test.go b/tests/e2e/create/create_test.go index 36525d9b..9e6595b5 100644 --- a/tests/e2e/create/create_test.go +++ b/tests/e2e/create/create_test.go @@ -135,6 +135,34 @@ var _ = Describe("Test 'create' command", Ordered, func() { }) }) + 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 resource link: 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 resource name: invalid Option: name must not be empty")) + }) + }) + Context("Given 'modulectl create' command", func() { var cmd createCmd It("When invoked with '--module-config-file' using valid file", func() { 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 From 99743f42820dda61097783ad6365d29af9acaaa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Thu, 17 Oct 2024 10:11:54 +0200 Subject: [PATCH 07/18] align tests --- internal/common/validation/validation.go | 23 ++++++++- internal/common/validation/validation_test.go | 47 +++++++++++++++++++ .../reader/moduleconfig_reader.go | 14 +----- .../reader/moduleconfig_reader_test.go | 32 ++++++++++++- tests/e2e/create/create_test.go | 8 ++-- 5 files changed, 104 insertions(+), 20 deletions(-) diff --git a/internal/common/validation/validation.go b/internal/common/validation/validation.go index 8a582254..77711719 100644 --- a/internal/common/validation/validation.go +++ b/internal/common/validation/validation.go @@ -9,6 +9,7 @@ import ( "github.com/Masterminds/semver/v3" commonerrors "github.com/kyma-project/modulectl/internal/common/errors" + "github.com/kyma-project/modulectl/internal/service/contentprovider" ) const ( @@ -100,14 +101,32 @@ func ValidateModuleNamespace(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: %s is not a valid URL", commonerrors.ErrInvalidOption, input) + return fmt.Errorf("%w: link %s is not a valid URL", commonerrors.ErrInvalidOption, input) } if _url.Scheme != "https" { - return fmt.Errorf("%w: %s is not using https scheme", commonerrors.ErrInvalidOption, input) + return fmt.Errorf("%w: link %s is not using https scheme", commonerrors.ErrInvalidOption, input) } return nil diff --git a/internal/common/validation/validation_test.go b/internal/common/validation/validation_test.go index a6904da2..021f9a55 100644 --- a/internal/common/validation/validation_test.go +++ b/internal/common/validation/validation_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/kyma-project/modulectl/internal/common/validation" + "github.com/kyma-project/modulectl/internal/service/contentprovider" ) func TestValidateModuleName(t *testing.T) { @@ -181,6 +182,52 @@ func TestValidateModuleNamespace(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 diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader.go b/internal/service/moduleconfig/reader/moduleconfig_reader.go index 18ab9be1..3931273e 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader.go @@ -144,18 +144,8 @@ func ValidateModuleConfig(moduleConfig *contentprovider.ModuleConfig) error { return fmt.Errorf("failed to validate module namespace: %w", err) } - for name, link := range moduleConfig.Resources { - if name == "" { - return fmt.Errorf("failed to validate resource name: %w: name must not be empty", commonerrors.ErrInvalidOption) - } - - if link == "" { - return fmt.Errorf("failed to validate resource link: %w: link must not be empty", commonerrors.ErrInvalidOption) - } - - if err := validation.ValidateIsValidHttpsUrl(link); err != nil { - return fmt.Errorf("failed to validate resource link: %w", err) - } + if err := validation.ValidateResources(moduleConfig.Resources); err != nil { + return fmt.Errorf("failed to validate resources: %w", err) } if moduleConfig.Manifest == "" { diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go index 458843cd..d3b93e97 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go @@ -235,7 +235,7 @@ func Test_ValidateModuleConfig(t *testing.T) { expectedError: fmt.Errorf("manifest must not be empty: %w", commonerrors.ErrInvalidOption), }, { - name: "invalid module resources - duplicate key", + name: "invalid module resources - not a URL", moduleConfig: &contentprovider.ModuleConfig{ Name: "github.com/module-name", Version: "0.0.1", @@ -246,7 +246,35 @@ func Test_ValidateModuleConfig(t *testing.T) { "key": "%% not a URL", }, }, - expectedError: fmt.Errorf("failed to validate resource link: invalid Option: %%%% not a URL is not a valid URL"), + expectedError: fmt.Errorf("failed to validate resources: invalid Option: link %%%% not a URL is not a valid URL"), + }, + { + 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: invalid Option: name must not be empty"), + }, + { + 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: invalid Option: link must not be empty"), }, } for _, test := range tests { diff --git a/tests/e2e/create/create_test.go b/tests/e2e/create/create_test.go index 9e6595b5..9d92b8cf 100644 --- a/tests/e2e/create/create_test.go +++ b/tests/e2e/create/create_test.go @@ -117,7 +117,7 @@ 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 validate module config: failed to validate resource name: invalid Option: name must not be empty")) + 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")) }) }) @@ -131,7 +131,7 @@ 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 validate resource link: invalid Option: http://some.other/location/template-operator.yaml is not using https scheme")) + 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")) }) }) @@ -145,7 +145,7 @@ 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 validate module config: failed to validate resource link: invalid Option: link must not be empty")) + 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")) }) }) @@ -159,7 +159,7 @@ 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 validate module config: failed to validate resource name: invalid Option: name must not be empty")) + 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")) }) }) From 081d5eef62fbb12b0293acf36d353d785e85191a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Thu, 17 Oct 2024 10:56:49 +0200 Subject: [PATCH 08/18] test marshalling and update coverage file --- .../contentprovider/moduleconfig_test.go | 62 +++++++++++++++++++ unit-test-coverage.yaml | 8 +-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/internal/service/contentprovider/moduleconfig_test.go b/internal/service/contentprovider/moduleconfig_test.go index 4647e971..684b5ecd 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,67 @@ func Test_ModuleConfig_GetDefaultContent_ReturnsConvertedContent(t *testing.T) { assert.Equal(t, mcConvertedContent, result) } +func Test_ModuleConfig_Unmarshal_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) + + assert.NoError(t, err) + assert.Equal(t, 2, len(moduleConfig.Resources)) + assert.Equal(t, "https://example.com/resource1", moduleConfig.Resources["resource1"]) + assert.Equal(t, "https://example.com/resource2", moduleConfig.Resources["resource2"]) +} + +func Test_ModuleConfig_Unmarshal_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) + + assert.Error(t, err) + assert.Equal(t, "list contains duplicate entries", err.Error()) +} + +func Test_ModuleConfig_Marshal_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{} + yaml.Unmarshal([]byte(expectedModuleConfigData), expectedModuleConfig) + + // 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) + roudTrippedModuleConfig := &contentprovider.ModuleConfig{} + yaml.Unmarshal([]byte(marshalledModuleConfigData), roudTrippedModuleConfig) + + assert.NoError(t, err) + assert.Equal(t, expectedModuleConfig.Resources, roudTrippedModuleConfig.Resources) +} + // Test Stubs type mcObjectToYAMLConverterStub struct{} diff --git a/unit-test-coverage.yaml b/unit-test-coverage.yaml index 80773ba3..294d7a94 100644 --- a/unit-test-coverage.yaml +++ b/unit-test-coverage.yaml @@ -2,13 +2,13 @@ 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/moduleconfig/generator: 100 - internal/service/moduleconfig/reader: 45 + internal/service/moduleconfig/reader: 50 internal/service/create: 34 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 From d4562877ad99a19b3234eae43c2d000077c659f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Thu, 17 Oct 2024 11:59:26 +0200 Subject: [PATCH 09/18] fix linting --- internal/common/validation/validation.go | 4 ++-- internal/common/validation/validation_test.go | 2 +- internal/service/contentprovider/moduleconfig.go | 7 +++++-- .../service/contentprovider/moduleconfig_test.go | 15 +++++++++------ .../reader/moduleconfig_reader_test.go | 6 +++--- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/internal/common/validation/validation.go b/internal/common/validation/validation.go index 77711719..fbf9450f 100644 --- a/internal/common/validation/validation.go +++ b/internal/common/validation/validation.go @@ -111,7 +111,7 @@ func ValidateResources(resources contentprovider.ResourcesMap) error { return fmt.Errorf("%w: link must not be empty", commonerrors.ErrInvalidOption) } - if err := ValidateIsValidHttpsUrl(link); err != nil { + if err := ValidateIsValidHTTPSURL(link); err != nil { return err } } @@ -119,7 +119,7 @@ func ValidateResources(resources contentprovider.ResourcesMap) error { return nil } -func ValidateIsValidHttpsUrl(input string) error { +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) diff --git a/internal/common/validation/validation_test.go b/internal/common/validation/validation_test.go index 021f9a55..38656520 100644 --- a/internal/common/validation/validation_test.go +++ b/internal/common/validation/validation_test.go @@ -267,7 +267,7 @@ func TestValidateIsValidHttpsUrl(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := validation.ValidateIsValidHttpsUrl(tt.url); (err != nil) != tt.wantErr { + 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 ff7db6c3..342edadf 100644 --- a/internal/service/contentprovider/moduleconfig.go +++ b/internal/service/contentprovider/moduleconfig.go @@ -1,12 +1,15 @@ package contentprovider import ( + "errors" "fmt" commonerrors "github.com/kyma-project/modulectl/internal/common/errors" "github.com/kyma-project/modulectl/internal/common/types" ) +var ErrDuplicateResourceNames = errors.New("list contains duplicate entries") + type ModuleConfigProvider struct { yamlConverter ObjectToYAMLConverter } @@ -94,7 +97,7 @@ type resource struct { type ResourcesMap map[string]string func (rm *ResourcesMap) UnmarshalYAML(unmarshal func(interface{}) error) error { - var resources []resource + resources := []resource{} if err := unmarshal(&resources); err != nil { return err } @@ -105,7 +108,7 @@ func (rm *ResourcesMap) UnmarshalYAML(unmarshal func(interface{}) error) error { } if len(resources) > len(*rm) { - return fmt.Errorf("list contains duplicate entries") + return ErrDuplicateResourceNames } return nil diff --git a/internal/service/contentprovider/moduleconfig_test.go b/internal/service/contentprovider/moduleconfig_test.go index 684b5ecd..d2a90954 100644 --- a/internal/service/contentprovider/moduleconfig_test.go +++ b/internal/service/contentprovider/moduleconfig_test.go @@ -147,8 +147,8 @@ resources: moduleConfig := &contentprovider.ModuleConfig{} err := yaml.Unmarshal([]byte(moduleConfigData), moduleConfig) - assert.NoError(t, err) - assert.Equal(t, 2, len(moduleConfig.Resources)) + 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"]) } @@ -165,7 +165,7 @@ resources: moduleConfig := &contentprovider.ModuleConfig{} err := yaml.Unmarshal([]byte(moduleConfigData), moduleConfig) - assert.Error(t, err) + require.Error(t, err) assert.Equal(t, "list contains duplicate entries", err.Error()) } @@ -179,7 +179,8 @@ resources: link: https://example.com/resource2 ` expectedModuleConfig := &contentprovider.ModuleConfig{} - yaml.Unmarshal([]byte(expectedModuleConfigData), expectedModuleConfig) + err := yaml.Unmarshal([]byte(expectedModuleConfigData), expectedModuleConfig) + require.NoError(t, err) // round trip a module config (marshal and unmarshal) moduleConfig := &contentprovider.ModuleConfig{ @@ -189,10 +190,12 @@ resources: }, } marshalledModuleConfigData, err := yaml.Marshal(moduleConfig) + require.NoError(t, err) + roudTrippedModuleConfig := &contentprovider.ModuleConfig{} - yaml.Unmarshal([]byte(marshalledModuleConfigData), roudTrippedModuleConfig) + err = yaml.Unmarshal([]byte(marshalledModuleConfigData), roudTrippedModuleConfig) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, expectedModuleConfig.Resources, roudTrippedModuleConfig.Resources) } diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go index d3b93e97..8bbecae3 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go @@ -246,7 +246,7 @@ func Test_ValidateModuleConfig(t *testing.T) { "key": "%% not a URL", }, }, - expectedError: fmt.Errorf("failed to validate resources: invalid Option: link %%%% not a URL is not a valid 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", @@ -260,7 +260,7 @@ func Test_ValidateModuleConfig(t *testing.T) { "": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", }, }, - expectedError: fmt.Errorf("failed to validate resources: invalid Option: name must not be empty"), + expectedError: fmt.Errorf("failed to validate resources: %s: name must not be empty", commonerrors.ErrInvalidOption), }, { name: "invalid module resources - empty link", @@ -274,7 +274,7 @@ func Test_ValidateModuleConfig(t *testing.T) { "name": "", }, }, - expectedError: fmt.Errorf("failed to validate resources: invalid Option: link must not be empty"), + expectedError: fmt.Errorf("failed to validate resources: %w: link must not be empty", commonerrors.ErrInvalidOption), }, } for _, test := range tests { From 2e63e9ebe2ba93aa7278288d3c7fc1c4ead64c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Thu, 17 Oct 2024 12:00:20 +0200 Subject: [PATCH 10/18] update error message --- internal/service/contentprovider/moduleconfig.go | 2 +- internal/service/contentprovider/moduleconfig_test.go | 2 +- tests/e2e/create/create_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/service/contentprovider/moduleconfig.go b/internal/service/contentprovider/moduleconfig.go index 342edadf..9b2a31f0 100644 --- a/internal/service/contentprovider/moduleconfig.go +++ b/internal/service/contentprovider/moduleconfig.go @@ -8,7 +8,7 @@ import ( "github.com/kyma-project/modulectl/internal/common/types" ) -var ErrDuplicateResourceNames = errors.New("list contains duplicate entries") +var ErrDuplicateResourceNames = errors.New("resources contain duplicate entries") type ModuleConfigProvider struct { yamlConverter ObjectToYAMLConverter diff --git a/internal/service/contentprovider/moduleconfig_test.go b/internal/service/contentprovider/moduleconfig_test.go index d2a90954..3f370f6d 100644 --- a/internal/service/contentprovider/moduleconfig_test.go +++ b/internal/service/contentprovider/moduleconfig_test.go @@ -166,7 +166,7 @@ resources: err := yaml.Unmarshal([]byte(moduleConfigData), moduleConfig) require.Error(t, err) - assert.Equal(t, "list contains duplicate entries", err.Error()) + assert.Equal(t, "resources contain duplicate entries", err.Error()) } func Test_ModuleConfig_Marshal_Resources_Success(t *testing.T) { diff --git a/tests/e2e/create/create_test.go b/tests/e2e/create/create_test.go index 9d92b8cf..913ce01c 100644 --- a/tests/e2e/create/create_test.go +++ b/tests/e2e/create/create_test.go @@ -103,7 +103,7 @@ 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 file: list contains duplicate entries")) + Expect(err.Error()).Should(ContainSubstring("failed to parse module config file: resources contain duplicate entries")) }) }) From fe49866dbc25bfc4e17f20dce97896869cacb743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Thu, 17 Oct 2024 12:07:15 +0200 Subject: [PATCH 11/18] fix linting --- internal/service/contentprovider/moduleconfig.go | 2 +- internal/service/contentprovider/moduleconfig_test.go | 2 +- .../service/moduleconfig/reader/moduleconfig_reader_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/service/contentprovider/moduleconfig.go b/internal/service/contentprovider/moduleconfig.go index 9b2a31f0..896bd0f6 100644 --- a/internal/service/contentprovider/moduleconfig.go +++ b/internal/service/contentprovider/moduleconfig.go @@ -115,7 +115,7 @@ func (rm *ResourcesMap) UnmarshalYAML(unmarshal func(interface{}) error) error { } func (rm ResourcesMap) MarshalYAML() (interface{}, error) { - var resources []resource + resources := []resource{} for name, link := range rm { resources = append(resources, resource{Name: name, Link: link}) } diff --git a/internal/service/contentprovider/moduleconfig_test.go b/internal/service/contentprovider/moduleconfig_test.go index 3f370f6d..8049b869 100644 --- a/internal/service/contentprovider/moduleconfig_test.go +++ b/internal/service/contentprovider/moduleconfig_test.go @@ -193,7 +193,7 @@ resources: require.NoError(t, err) roudTrippedModuleConfig := &contentprovider.ModuleConfig{} - err = yaml.Unmarshal([]byte(marshalledModuleConfigData), roudTrippedModuleConfig) + err = yaml.Unmarshal(marshalledModuleConfigData, roudTrippedModuleConfig) require.NoError(t, err) assert.Equal(t, expectedModuleConfig.Resources, roudTrippedModuleConfig.Resources) diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go index 8bbecae3..28dfd0b8 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go @@ -260,7 +260,7 @@ func Test_ValidateModuleConfig(t *testing.T) { "": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", }, }, - expectedError: fmt.Errorf("failed to validate resources: %s: name must not be empty", commonerrors.ErrInvalidOption), + expectedError: fmt.Errorf("failed to validate resources: %w: name must not be empty", commonerrors.ErrInvalidOption), }, { name: "invalid module resources - empty link", From 672a36aa27086ea50f0120c97d3f120c6204fff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Thu, 17 Oct 2024 12:17:37 +0200 Subject: [PATCH 12/18] add test for unknown fields --- .../contentprovider/moduleconfig_test.go | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/internal/service/contentprovider/moduleconfig_test.go b/internal/service/contentprovider/moduleconfig_test.go index 8049b869..31e67e2e 100644 --- a/internal/service/contentprovider/moduleconfig_test.go +++ b/internal/service/contentprovider/moduleconfig_test.go @@ -135,7 +135,7 @@ func Test_ModuleConfig_GetDefaultContent_ReturnsConvertedContent(t *testing.T) { assert.Equal(t, mcConvertedContent, result) } -func Test_ModuleConfig_Unmarshal_Resources_Success(t *testing.T) { +func Test_ModuleConfig_Unmarshall_Resources_Success(t *testing.T) { moduleConfigData := ` resources: - name: resource1 @@ -153,7 +153,23 @@ resources: assert.Equal(t, "https://example.com/resource2", moduleConfig.Resources["resource2"]) } -func Test_ModuleConfig_Unmarshal_Resources_FailOnDuplicateNames(t *testing.T) { +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 @@ -169,7 +185,7 @@ resources: assert.Equal(t, "resources contain duplicate entries", err.Error()) } -func Test_ModuleConfig_Marshal_Resources_Success(t *testing.T) { +func Test_ModuleConfig_Marshall_Resources_Success(t *testing.T) { // parse the expected config expectedModuleConfigData := ` resources: From 60cebf8fd412ccf3812204c2e6b1958cc31ac0ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Fri, 18 Oct 2024 16:21:26 +0200 Subject: [PATCH 13/18] chore: Refactor file resolving --- cmd/modulectl/cmd.go | 16 ++- .../service/contentprovider/moduleconfig.go | 43 +++---- internal/service/create/create.go | 65 ++++++++-- internal/service/fileresolver/fileresolver.go | 74 +++++++++++ .../reader/moduleconfig_reader.go | 121 +----------------- 5 files changed, 167 insertions(+), 152 deletions(-) create mode 100644 internal/service/fileresolver/fileresolver.go 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/internal/service/contentprovider/moduleconfig.go b/internal/service/contentprovider/moduleconfig.go index b3ff477e..b7a752eb 100644 --- a/internal/service/contentprovider/moduleconfig.go +++ b/internal/service/contentprovider/moduleconfig.go @@ -38,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], - Manifest: 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], } } @@ -80,22 +80,21 @@ type Manager struct { } type ModuleConfig struct { - Name string `yaml:"name" comment:"required, the name of the Module"` - Version string `yaml:"version" comment:"required, the version of the Module"` - Channel string `yaml:"channel" comment:"required, channel that should be used in the ModuleTemplate"` - Manifest string `yaml:"manifest" comment:"required, relative path or remote URL to the manifests"` - ManifestFilePath string `yaml:"-"` // ignore this field, will be filled programmatically - Mandatory bool `yaml:"mandatory" comment:"optional, default=false, indicates whether the module is mandatory to be installed on all clusters"` - DefaultCRPath string `yaml:"defaultCR" comment:"optional, relative path or remote URL to a YAML file containing the default CR for the module"` - ResourceName string `yaml:"resourceName" comment:"optional, default={name}-{channel}, when channel is 'none', the default is {name}-{version}, the name for the ModuleTemplate that will be created"` - Namespace string `yaml:"namespace" comment:"optional, default=kcp-system, the namespace where the ModuleTemplate will be deployed"` - Security string `yaml:"security" comment:"optional, name of the security scanners config file"` - Internal bool `yaml:"internal" comment:"optional, default=false, determines whether the ModuleTemplate should have the internal flag or not"` - Beta bool `yaml:"beta" comment:"optional, default=false, determines whether the ModuleTemplate should have the beta flag or not"` - Labels map[string]string `yaml:"labels" comment:"optional, additional labels for the ModuleTemplate"` - Annotations map[string]string `yaml:"annotations" comment:"optional, additional annotations for 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"` + Name string `yaml:"name" comment:"required, the name of the Module"` + Version string `yaml:"version" comment:"required, the version of the Module"` + Channel string `yaml:"channel" comment:"required, channel that should be used in the ModuleTemplate"` + Manifest string `yaml:"manifest" comment:"required, relative path or remote URL to the manifests"` + Mandatory bool `yaml:"mandatory" comment:"optional, default=false, indicates whether the module is mandatory to be installed on all clusters"` + DefaultCR string `yaml:"defaultCR" comment:"optional, relative path or remote URL to a YAML file containing the default CR for the module"` + ResourceName string `yaml:"resourceName" comment:"optional, default={name}-{channel}, when channel is 'none', the default is {name}-{version}, the name for the ModuleTemplate that will be created"` + Namespace string `yaml:"namespace" comment:"optional, default=kcp-system, the namespace where the ModuleTemplate will be deployed"` + Security string `yaml:"security" comment:"optional, name of the security scanners config file"` + Internal bool `yaml:"internal" comment:"optional, default=false, determines whether the ModuleTemplate should have the internal flag or not"` + Beta bool `yaml:"beta" comment:"optional, default=false, determines whether the ModuleTemplate should have the beta flag or not"` + Labels map[string]string `yaml:"labels" comment:"optional, additional labels for the ModuleTemplate"` + Annotations map[string]string `yaml:"annotations" comment:"optional, additional annotations for 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 { diff --git a/internal/service/create/create.go b/internal/service/create/create.go index 82adf206..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.ManifestFilePath, - 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.ManifestFilePath) + 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/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/moduleconfig/reader/moduleconfig_reader.go b/internal/service/moduleconfig/reader/moduleconfig_reader.go index 3cadb0db..a4889f32 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" @@ -16,37 +13,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 } @@ -61,72 +42,9 @@ func (s *Service) ParseAndValidateModuleConfig(moduleConfigFile string, return nil, fmt.Errorf("failed to validate 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.ManifestFilePath, err = GetManifestPath(moduleConfig.Manifest, s.tempFileSystem) - if err != nil { - return nil, fmt.Errorf("failed to get manifest path: %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) @@ -206,36 +124,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 -} From bcc8a5254f84fe23c7d33ee6d2c768eb8f224fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Fri, 18 Oct 2024 17:05:22 +0200 Subject: [PATCH 14/18] fix unit tests --- internal/service/create/create_test.go | 94 ++++++++--- .../service/fileresolver/fileresolver_test.go | 116 +++++++++++++ .../generator/moduleconfig_generator_test.go | 26 +-- .../reader/moduleconfig_reader_test.go | 157 ++---------------- tests/e2e/create/create_suite_test.go | 2 +- tests/e2e/scaffold/scaffold_suite_test.go | 2 +- unit-test-coverage.yaml | 2 +- 7 files changed, 218 insertions(+), 181 deletions(-) create mode 100644 internal/service/fileresolver/fileresolver_test.go 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_test.go b/internal/service/fileresolver/fileresolver_test.go new file mode 100644 index 00000000..39e237fa --- /dev/null +++ b/internal/service/fileresolver/fileresolver_test.go @@ -0,0 +1,116 @@ +package fileresolver_test + +import ( + "fmt" + "net/url" + "testing" + + commonerrors "github.com/kyma-project/modulectl/internal/common/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "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_CorrectPath_When_NotUrl(t *testing.T) { + resolver, _ := fileresolver.NewFileResolver(filePattern, &tmpfileSystemStub{}) + result, err := resolver.Resolve("/path/to/manifest.yaml") + + 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) { + 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 +} diff --git a/internal/service/moduleconfig/generator/moduleconfig_generator_test.go b/internal/service/moduleconfig/generator/moduleconfig_generator_test.go index af614744..1ee8ed2c 100644 --- a/internal/service/moduleconfig/generator/moduleconfig_generator_test.go +++ b/internal/service/moduleconfig/generator/moduleconfig_generator_test.go @@ -118,19 +118,19 @@ func (*fileExistsStub) FileExists(_ string) (bool, error) { func (*fileExistsStub) ReadFile(_ string) ([]byte, error) { moduleConfig := contentprovider.ModuleConfig{ - Name: "module-name", - Version: "0.0.1", - Channel: "regular", - Manifest: "path/to/manifests", - Mandatory: false, - DefaultCRPath: "path/to/defaultCR", - ResourceName: "module-name-0.0.1", - Namespace: "kcp-system", - Security: "path/to/securityConfig", - Internal: false, - Beta: false, - Labels: map[string]string{"label1": "value1"}, - Annotations: map[string]string{"annotation1": "value1"}, + Name: "module-name", + Version: "0.0.1", + Channel: "regular", + Manifest: "path/to/manifests", + Mandatory: false, + DefaultCR: "path/to/defaultCR", + ResourceName: "module-name-0.0.1", + Namespace: "kcp-system", + Security: "path/to/securityConfig", + Internal: false, + Beta: false, + Labels: map[string]string{"label1": "value1"}, + Annotations: map[string]string{"annotation1": "value1"}, Manager: &contentprovider.Manager{ Name: "manager-name", Namespace: "manager-namespace", diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go index f63f640d..76228552 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" @@ -34,7 +33,7 @@ func Test_ParseModuleConfig_Returns_CorrectModuleConfig(t *testing.T) { require.Equal(t, "0.0.1", result.Version) require.Equal(t, "regular", result.Channel) require.Equal(t, "path/to/manifests", result.Manifest) - require.Equal(t, "path/to/defaultCR", result.DefaultCRPath) + 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) @@ -54,124 +53,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) { @@ -402,19 +285,19 @@ func (*fileExistsStub) FileExists(_ string) (bool, error) { } var expectedReturnedModuleConfig = contentprovider.ModuleConfig{ - Name: "github.com/module-name", - Version: "0.0.1", - Channel: "regular", - Manifest: "path/to/manifests", - Mandatory: false, - DefaultCRPath: "path/to/defaultCR", - ResourceName: "module-name-0.0.1", - Namespace: "kcp-system", - Security: "path/to/securityConfig", - Internal: false, - Beta: false, - Labels: map[string]string{"label1": "value1"}, - Annotations: map[string]string{"annotation1": "value1"}, + Name: "github.com/module-name", + Version: "0.0.1", + Channel: "regular", + Manifest: "path/to/manifests", + Mandatory: false, + DefaultCR: "path/to/defaultCR", + ResourceName: "module-name-0.0.1", + Namespace: "kcp-system", + Security: "path/to/securityConfig", + Internal: false, + Beta: false, + Labels: map[string]string{"label1": "value1"}, + Annotations: map[string]string{"annotation1": "value1"}, Resources: contentprovider.ResourcesMap{ "rawManifest": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", }, @@ -433,16 +316,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/tests/e2e/create/create_suite_test.go b/tests/e2e/create/create_suite_test.go index da6ecf3c..eaebc318 100644 --- a/tests/e2e/create/create_suite_test.go +++ b/tests/e2e/create/create_suite_test.go @@ -84,7 +84,7 @@ func (cmd *createCmd) execute() error { println(" >>> Executing command: modulectl", strings.Join(args, " ")) - command = exec.Command("modulectl", args...) + command = exec.Command("/Users/I507792/Workspaces/go/src/github.com/c-pius/modulectl/bin/modulectl-darwin-arm", args...) cmdOut, err := command.CombinedOutput() if err != nil { return fmt.Errorf("create command failed with output: %s and error: %w", cmdOut, err) diff --git a/tests/e2e/scaffold/scaffold_suite_test.go b/tests/e2e/scaffold/scaffold_suite_test.go index a666607d..7f5c878c 100644 --- a/tests/e2e/scaffold/scaffold_suite_test.go +++ b/tests/e2e/scaffold/scaffold_suite_test.go @@ -66,7 +66,7 @@ func (cmd *scaffoldCmd) execute() error { args = append(args, "--overwrite=true") } - command = exec.Command("modulectl", args...) + command = exec.Command("/Users/I507792/Workspaces/go/src/github.com/c-pius/modulectl/bin/modulectl-darwin-arm", args...) cmdOut, err := command.CombinedOutput() if err != nil { return fmt.Errorf("scaffold command failed with output: %s and error: %w", cmdOut, err) diff --git a/unit-test-coverage.yaml b/unit-test-coverage.yaml index 294d7a94..f9285c7c 100644 --- a/unit-test-coverage.yaml +++ b/unit-test-coverage.yaml @@ -6,7 +6,7 @@ packages: internal/service/filegenerator: 100 internal/service/moduleconfig/generator: 100 internal/service/moduleconfig/reader: 50 - internal/service/create: 34 + internal/service/create: 43 internal/service/componentdescriptor: 78 internal/service/templategenerator: 78 internal/service/crdparser: 73 From 6996756b2e04ed7368dff921ce01d301fac1c729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Fri, 18 Oct 2024 17:06:49 +0200 Subject: [PATCH 15/18] revert suite test changes --- tests/e2e/create/create_suite_test.go | 2 +- tests/e2e/scaffold/scaffold_suite_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/create/create_suite_test.go b/tests/e2e/create/create_suite_test.go index eaebc318..da6ecf3c 100644 --- a/tests/e2e/create/create_suite_test.go +++ b/tests/e2e/create/create_suite_test.go @@ -84,7 +84,7 @@ func (cmd *createCmd) execute() error { println(" >>> Executing command: modulectl", strings.Join(args, " ")) - command = exec.Command("/Users/I507792/Workspaces/go/src/github.com/c-pius/modulectl/bin/modulectl-darwin-arm", args...) + command = exec.Command("modulectl", args...) cmdOut, err := command.CombinedOutput() if err != nil { return fmt.Errorf("create command failed with output: %s and error: %w", cmdOut, err) diff --git a/tests/e2e/scaffold/scaffold_suite_test.go b/tests/e2e/scaffold/scaffold_suite_test.go index 7f5c878c..a666607d 100644 --- a/tests/e2e/scaffold/scaffold_suite_test.go +++ b/tests/e2e/scaffold/scaffold_suite_test.go @@ -66,7 +66,7 @@ func (cmd *scaffoldCmd) execute() error { args = append(args, "--overwrite=true") } - command = exec.Command("/Users/I507792/Workspaces/go/src/github.com/c-pius/modulectl/bin/modulectl-darwin-arm", args...) + command = exec.Command("modulectl", args...) cmdOut, err := command.CombinedOutput() if err != nil { return fmt.Errorf("scaffold command failed with output: %s and error: %w", cmdOut, err) From 65254d9b45d9c262059b093b3d0340c2f78f5ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Fri, 18 Oct 2024 17:21:52 +0200 Subject: [PATCH 16/18] increase fileresolver coverage --- .../service/fileresolver/fileresolver_test.go | 32 +++++++++++++++++-- unit-test-coverage.yaml | 1 + 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/internal/service/fileresolver/fileresolver_test.go b/internal/service/fileresolver/fileresolver_test.go index 39e237fa..ff6a0d08 100644 --- a/internal/service/fileresolver/fileresolver_test.go +++ b/internal/service/fileresolver/fileresolver_test.go @@ -41,12 +41,30 @@ func Test_Resolve_Returns_CorrectPath(t *testing.T) { require.Equal(t, "file.yaml", result) } -func Test_Resolve_Returns_CorrectPath_When_NotUrl(t *testing.T) { +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) - require.Equal(t, "/path/to/manifest.yaml", result) + 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) { @@ -114,3 +132,13 @@ func (*tmpfileSystemStub) DownloadTempFile(_ string, _ string, _ *url.URL) (stri func (s *tmpfileSystemStub) RemoveTempFiles() []error { return nil } + +type tempfileSystemErrorStub struct{} + +func (*tempfileSystemErrorStub) DownloadTempFile(_ string, _ string, _ *url.URL) (string, error) { + return "", fmt.Errorf("error downloading file") +} + +func (s *tempfileSystemErrorStub) RemoveTempFiles() []error { + return nil +} diff --git a/unit-test-coverage.yaml b/unit-test-coverage.yaml index f9285c7c..e791aeab 100644 --- a/unit-test-coverage.yaml +++ b/unit-test-coverage.yaml @@ -4,6 +4,7 @@ packages: internal/service/scaffold: 92 internal/service/contentprovider: 98 internal/service/filegenerator: 100 + internal/service/fileresolver: 92 internal/service/moduleconfig/generator: 100 internal/service/moduleconfig/reader: 50 internal/service/create: 43 From 5284bf47bb2ffc0c61773deb6c773b0717e848cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Fri, 18 Oct 2024 17:24:07 +0200 Subject: [PATCH 17/18] fix linting --- internal/service/fileresolver/fileresolver_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/service/fileresolver/fileresolver_test.go b/internal/service/fileresolver/fileresolver_test.go index ff6a0d08..794fe29a 100644 --- a/internal/service/fileresolver/fileresolver_test.go +++ b/internal/service/fileresolver/fileresolver_test.go @@ -5,10 +5,11 @@ import ( "net/url" "testing" - commonerrors "github.com/kyma-project/modulectl/internal/common/errors" + "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" ) @@ -64,7 +65,7 @@ func Test_Resolve_Returns_CorrectPath_When_Relative(t *testing.T) { require.NoError(t, err) assert.Contains(t, result, "/path/to/manifest.yaml") - assert.Equal(t, rune(result[0]), '/') + assert.Equal(t, '/', rune(result[0])) } func TestService_ParseURL(t *testing.T) { @@ -136,7 +137,7 @@ func (s *tmpfileSystemStub) RemoveTempFiles() []error { type tempfileSystemErrorStub struct{} func (*tempfileSystemErrorStub) DownloadTempFile(_ string, _ string, _ *url.URL) (string, error) { - return "", fmt.Errorf("error downloading file") + return "", errors.New("error downloading file") } func (s *tempfileSystemErrorStub) RemoveTempFiles() []error { From c22e291abac5d420566726625ff38248273140dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Mon, 21 Oct 2024 13:28:24 +0200 Subject: [PATCH 18/18] remove named import --- tests/e2e/create/create_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/create/create_test.go b/tests/e2e/create/create_test.go index b991bb8b..4bb760cc 100644 --- a/tests/e2e/create/create_test.go +++ b/tests/e2e/create/create_test.go @@ -10,7 +10,7 @@ import ( "ocm.software/ocm/api/ocm" "ocm.software/ocm/api/ocm/compdesc" ocmv1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" - v2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" + "ocm.software/ocm/api/ocm/compdesc/versions/v2" "ocm.software/ocm/api/ocm/extensions/accessmethods/github" "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob" "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"