diff --git a/.github/workflows/lint-conventional-prs.yml b/.github/workflows/lint-conventional-prs.yml index 333ebcfd..eece8e93 100644 --- a/.github/workflows/lint-conventional-prs.yml +++ b/.github/workflows/lint-conventional-prs.yml @@ -14,7 +14,7 @@ jobs: name: Lint PR Title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@47b15d52c5c30e94a17ec87eb8dd51ff5221fed9 + - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/lint-golangci.yml b/.github/workflows/lint-golangci.yml index d18fcf33..dce7db6e 100644 --- a/.github/workflows/lint-golangci.yml +++ b/.github/workflows/lint-golangci.yml @@ -17,7 +17,7 @@ jobs: go-version-file: 'go.mod' cache: false - name: golangci-lint - uses: golangci/golangci-lint-action@v6.1.0 + uses: golangci/golangci-lint-action@v6.1.1 with: version: v1.60.3 args: --verbose diff --git a/.github/workflows/test-unit-coverage.yml b/.github/workflows/test-unit-coverage.yml index 91799ec7..e2f8208c 100644 --- a/.github/workflows/test-unit-coverage.yml +++ b/.github/workflows/test-unit-coverage.yml @@ -28,7 +28,7 @@ jobs: with: path: codebase - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version-file: ${{ github.workspace }}/codebase/go.mod - name: Run the quality gate verification diff --git a/.github/workflows/validate-docs.yml b/.github/workflows/validate-docs.yml index fffdcf46..36810201 100644 --- a/.github/workflows/validate-docs.yml +++ b/.github/workflows/validate-docs.yml @@ -13,7 +13,7 @@ jobs: - name: Checkout modulectl uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version-file: 'go.mod' cache-dependency-path: 'go.sum' diff --git a/cmd/modulectl/create/long.txt b/cmd/modulectl/create/long.txt index 22555351..0e2dad5c 100644 --- a/cmd/modulectl/create/long.txt +++ b/cmd/modulectl/create/long.txt @@ -14,23 +14,35 @@ The module config file is a YAML file used to configure the following attributes - name: a string, required, the name of the module - version: a string, required, the version of the module - channel: a string, required, channel that should be used in the ModuleTemplate CR -- manifest: a string, required, reference to the manifest, must be a relative file name or URL -- info: - -- defaultCR: a string, optional, reference to a YAML file containing the default CR for the module, must be a relative file name or URL +- manifest: a string, required, reference to the manifest, must be a URL +- repository: a string, required, reference to the repository, must be a URL +- documentation: a string, required, reference to the documentation, must be a URL +- icons: a map with string keys and values, required, icons used for UI + - name: a string, required, the name of the icon + link: a URL, required, the link to the icon +- defaultCR: a string, optional, reference to a YAML file containing the default CR for the module, must be a URL - mandatory: a boolean, optional, default=false, indicates whether the module is mandatory to be installed on all clusters -- resourceName: a string, optional, default={NAME}-{CHANNEL}, the name for the ModuleTemplate CR that will be created - internal: a boolean, optional, default=false, determines whether the ModuleTemplate CR should have the internal flag or not - beta: a boolean, optional, default=false, determines whether the ModuleTemplate CR should have the beta flag or not - security: a string, optional, name of the security scanners config file - labels: a map with string keys and values, optional, additional labels for the generated ModuleTemplate CR - annotations: a map with string keys and values, optional, additional annotations for the generated ModuleTemplate CR -- resources : a map with string keys and values, optional, additional resources of the ModuleTemplate that may be fetched +- manager: # an object, optional, module resource that indicates the installation readiness of the module + name: a string, required, the name of the module resource + namespace: a string, optional, the namespace of the module resource + group: a string, required, the API group of the module resource + version: a string, required, the API version of the module resource + kind: a string, required, the API kind of the module resource +- associatedResources: a list of Group-Version-Kind(GVK), optional, resources that should be cleaned up with the module deletion +- resources: # a map with string keys and values, optional, additional resources of the module that may be fetched + - name: a string, required, the name of the resource + link: a URL, required, the link to the resource ``` The **manifest** file contains all the module's resources in a single, multi-document YAML file. These resources will be created in the Kyma cluster when the module is activated. The **defaultCR** file contains a default custom resource for the module that is installed along with the module. It is additionally schema-validated against the Custom Resource Definition. The CRD used for the validation must exist in the set of the module's resources. +The **resources** are copied to the ModuleTemplate **spec.resources**. If it does not have an entry named 'raw-manifest', the ModuleTemplate **spec.resources** populates this entry from the **manifest** field specified in the module config file. ### Modules as OCI artifacts Modules are built and distributed as OCI artifacts. diff --git a/cmd/modulectl/scaffold/cmd_test.go b/cmd/modulectl/scaffold/cmd_test.go index a97d78d8..52519837 100644 --- a/cmd/modulectl/scaffold/cmd_test.go +++ b/cmd/modulectl/scaffold/cmd_test.go @@ -56,7 +56,7 @@ func Test_Execute_ParsesOptions(t *testing.T) { os.Args = []string{ "scaffold", "--directory", directory, - "--module-config", moduleConfigFile, + "--config-file", moduleConfigFile, "--overwrite", "--gen-manifest", manifestFile, "--gen-default-cr=" + defaultCRFile, @@ -84,8 +84,10 @@ func Test_Execute_ParsesOptions(t *testing.T) { func Test_Execute_ParsesShortOptions(t *testing.T) { directory := testutils.RandomName(10) + configFile := testutils.RandomName(10) os.Args = []string{ "scaffold", + "-c", configFile, "-d", directory, "-o", } @@ -95,6 +97,7 @@ func Test_Execute_ParsesShortOptions(t *testing.T) { err := cmd.Execute() require.NoError(t, err) + assert.Equal(t, configFile, svc.opts.ModuleConfigFileName) assert.Equal(t, directory, svc.opts.Directory) assert.True(t, svc.opts.ModuleConfigFileOverwrite) } diff --git a/cmd/modulectl/scaffold/flags.go b/cmd/modulectl/scaffold/flags.go index 7c487053..8face32b 100644 --- a/cmd/modulectl/scaffold/flags.go +++ b/cmd/modulectl/scaffold/flags.go @@ -12,7 +12,8 @@ const ( DirectoryFlagDefault = "./" directoryFlagUsage = `Specifies the target directory where the scaffolding shall be generated (default "./").` - ModuleConfigFileFlagName = "module-config" + ModuleConfigFileFlagName = "config-file" + moduleConfigFileFlagShort = "c" ModuleConfigFileFlagDefault = "scaffold-module-config.yaml" moduleConfigFileFlagUsage = `Specifies the name of the generated module configuration file (default "scaffold-module-config.yaml").` @@ -50,7 +51,7 @@ const ( func parseFlags(flags *pflag.FlagSet, opts *scaffold.Options) { flags.StringVarP(&opts.Directory, DirectoryFlagName, directoryFlagShort, DirectoryFlagDefault, directoryFlagUsage) - flags.StringVar(&opts.ModuleConfigFileName, ModuleConfigFileFlagName, ModuleConfigFileFlagDefault, moduleConfigFileFlagUsage) + flags.StringVarP(&opts.ModuleConfigFileName, ModuleConfigFileFlagName, moduleConfigFileFlagShort, ModuleConfigFileFlagDefault, moduleConfigFileFlagUsage) flags.BoolVarP(&opts.ModuleConfigFileOverwrite, ModuleConfigFileOverwriteFlagName, moduleConfigFileOverwriteFlagShort, ModuleConfigFileOverwriteFlagDefault, moduleConfigFileOverwriteFlagUsage) flags.StringVar(&opts.ManifestFileName, ManifestFileFlagName, ManifestFileFlagDefault, manifestFileFlagUsage) flags.StringVar(&opts.DefaultCRFileName, DefaultCRFlagName, DefaultCRFlagDefault, defaultCRFlagUsage) diff --git a/cmd/modulectl/scaffold/long.txt b/cmd/modulectl/scaffold/long.txt index 42e9a721..d98c36ba 100644 --- a/cmd/modulectl/scaffold/long.txt +++ b/cmd/modulectl/scaffold/long.txt @@ -8,7 +8,7 @@ allowing for a tailored scaffolding experience according to the specific needs o The command generates or uses the following files: - Module Config: Enabled: Always - Adjustable with flag: --module-config=VALUE + Adjustable with flag: --config-file=VALUE Generated when: The file doesn't exist or the --overwrite=true flag is provided Default file name: scaffold-module-config.yaml - Manifest: diff --git a/docs/gen-docs/modulectl_create.md b/docs/gen-docs/modulectl_create.md index 5bf002fa..a02b2573 100644 --- a/docs/gen-docs/modulectl_create.md +++ b/docs/gen-docs/modulectl_create.md @@ -22,21 +22,31 @@ The module config file is a YAML file used to configure the following attributes - name: a string, required, the name of the module - version: a string, required, the version of the module - channel: a string, required, channel that should be used in the ModuleTemplate CR -- manifest: a string, required, reference to the manifest, must be a relative file name or URL +- manifest: a string, required, reference to the manifest, must be a URL -- defaultCR: a string, optional, reference to a YAML file containing the default CR for the module, must be a relative file name or URL +- defaultCR: a string, optional, reference to a YAML file containing the default CR for the module, must be a URL - mandatory: a boolean, optional, default=false, indicates whether the module is mandatory to be installed on all clusters -- resourceName: a string, optional, default={NAME}-{CHANNEL}, the name for the ModuleTemplate CR that will be created - internal: a boolean, optional, default=false, determines whether the ModuleTemplate CR should have the internal flag or not - beta: a boolean, optional, default=false, determines whether the ModuleTemplate CR should have the beta flag or not - security: a string, optional, name of the security scanners config file - labels: a map with string keys and values, optional, additional labels for the generated ModuleTemplate CR - annotations: a map with string keys and values, optional, additional annotations for the generated ModuleTemplate CR +- manager: # an object, optional, module resource that indicates the installation readiness of the module + name: a string, required, the name of the module resource + namespace: a string, optional, the namespace of the module resource + group: a string, required, the API group of the module resource + version: a string, required, the API version of the module resource + kind: a string, required, the API kind of the module resource +- associatedResources: a list of Group-Version-Kind(GVK), optional, resources that should be cleaned up with the module deletion +- resources: # a map with string keys and values, optional, additional resources of the module that may be fetched + - name: a string, required, the name of the resource + link: a URL, required, the link to the resource ``` The **manifest** file contains all the module's resources in a single, multi-document YAML file. These resources will be created in the Kyma cluster when the module is activated. The **defaultCR** file contains a default custom resource for the module that is installed along with the module. It is additionally schema-validated against the Custom Resource Definition. The CRD used for the validation must exist in the set of the module's resources. +The **resources** are copied to the ModuleTemplate **spec.resources**. If it does not have an entry named 'raw-manifest', the ModuleTemplate **spec.resources** populates this entry from the **manifest** field specified in the module config file. ### Modules as OCI artifacts Modules are built and distributed as OCI artifacts. diff --git a/docs/gen-docs/modulectl_scaffold.md b/docs/gen-docs/modulectl_scaffold.md index db629281..3f0f52e3 100644 --- a/docs/gen-docs/modulectl_scaffold.md +++ b/docs/gen-docs/modulectl_scaffold.md @@ -17,7 +17,7 @@ allowing for a tailored scaffolding experience according to the specific needs o The command generates or uses the following files: - Module Config: Enabled: Always - Adjustable with flag: --module-config=VALUE + Adjustable with flag: --config-file=VALUE Generated when: The file doesn't exist or the --overwrite=true flag is provided Default file name: scaffold-module-config.yaml - Manifest: @@ -69,13 +69,13 @@ Generate a scaffold with a manifest file, default CR and security-scanners confi ## Flags ```bash +-c, --config-file string Specifies the name of the generated module configuration file (default "scaffold-module-config.yaml"). -d, --directory string Specifies the target directory where the scaffolding shall be generated (default "./"). --gen-default-cr string Specifies the default CR in the generated module config. A blank default CR file is generated if it doesn't exist (default "default-cr.yaml"). --gen-manifest string Specifies the manifest in the generated module config. A blank manifest file is generated if it doesn't exist (default "manifest.yaml"). --gen-security-config string Specifies the security file in the generated module config. A scaffold security config file is generated if it doesn't exist (default "sec-scanners-config.yaml"). -h, --help Provides help for the scaffold command. --module-channel string Specifies the module channel in the generated module config file (default "regular"). - --module-config string Specifies the name of the generated module configuration file (default "scaffold-module-config.yaml"). --module-name string Specifies the module name in the generated config file (default "kyma-project.io/module/mymodule"). --module-version string Specifies the module version in the generated module config file (default "0.0.1"). -o, --overwrite Specifies if the command overwrites an existing module configuration file. diff --git a/go.mod b/go.mod index 1e435fcd..d063c79a 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Masterminds/semver/v3 v3.3.0 github.com/go-git/go-git/v5 v5.12.0 github.com/kyma-project/lifecycle-manager/api v0.0.0-20241016122945-f0ac9bb8aa0c + github.com/mandelsoft/goutils v0.0.0-20240915132328-95975bffaef0 github.com/mandelsoft/vfs v0.4.4 github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.34.2 @@ -13,8 +14,8 @@ require ( github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/apiextensions-apiserver v0.31.1 - k8s.io/apimachinery v0.31.1 + k8s.io/apiextensions-apiserver v0.31.2 + k8s.io/apimachinery v0.31.2 ocm.software/ocm v0.16.2 sigs.k8s.io/yaml v1.4.0 ) @@ -196,7 +197,6 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mandelsoft/filepath v0.0.0-20240223090642-3e2777258aa3 // indirect - github.com/mandelsoft/goutils v0.0.0-20240915132328-95975bffaef0 // indirect github.com/mandelsoft/logging v0.0.0-20240618075559-fdca28a87b0a // indirect github.com/mandelsoft/spiff v1.7.0-beta-5 // indirect github.com/marstr/guid v1.1.0 // indirect @@ -324,9 +324,9 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect helm.sh/helm/v3 v3.16.1 // indirect - k8s.io/api v0.31.1 // indirect + k8s.io/api v0.31.2 // indirect k8s.io/cli-runtime v0.31.1 // indirect - k8s.io/client-go v0.31.1 // indirect + k8s.io/client-go v0.31.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect diff --git a/go.sum b/go.sum index 339045c5..e40493d1 100644 --- a/go.sum +++ b/go.sum @@ -1312,18 +1312,18 @@ helm.sh/helm/v3 v3.16.1 h1:cER6tI/8PgUAsaJaQCVBUg3VI9KN4oVaZJgY60RIc0c= helm.sh/helm/v3 v3.16.1/go.mod h1:r+xBHHP20qJeEqtvBXMf7W35QDJnzY/eiEBzt+TfHps= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= -k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= -k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40= -k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ= -k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= -k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= +k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= +k8s.io/apiextensions-apiserver v0.31.2 h1:W8EwUb8+WXBLu56ser5IudT2cOho0gAKeTOnywBLxd0= +k8s.io/apiextensions-apiserver v0.31.2/go.mod h1:i+Geh+nGCJEGiCGR3MlBDkS7koHIIKWVfWeRFiOsUcM= +k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= +k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= k8s.io/cli-runtime v0.31.1 h1:/ZmKhmZ6hNqDM+yf9s3Y4KEYakNXUn5sod2LWGGwCuk= k8s.io/cli-runtime v0.31.1/go.mod h1:pKv1cDIaq7ehWGuXQ+A//1OIF+7DI+xudXtExMCbe9U= -k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= -k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= -k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8= -k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w= +k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= +k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= +k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA= +k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= diff --git a/internal/common/validation/validation.go b/internal/common/validation/validation.go index 1b2b1394..923a3461 100644 --- a/internal/common/validation/validation.go +++ b/internal/common/validation/validation.go @@ -152,3 +152,19 @@ func validateSemanticVersion(version string) error { return nil } + +func ValidateGvk(group, version, kind string) error { + if kind == "" { + return fmt.Errorf("kind must not be empty: %w", commonerrors.ErrInvalidOption) + } + + if group == "" { + return fmt.Errorf("group must not be empty: %w", commonerrors.ErrInvalidOption) + } + + if version == "" { + return fmt.Errorf("version must not be empty: %w", commonerrors.ErrInvalidOption) + } + + return nil +} diff --git a/internal/common/validation/validation_test.go b/internal/common/validation/validation_test.go index c68e7386..ccd5ceea 100644 --- a/internal/common/validation/validation_test.go +++ b/internal/common/validation/validation_test.go @@ -227,6 +227,47 @@ func TestValidateNamespace(t *testing.T) { } } +func TestValidateGvk(t *testing.T) { + type args struct { + group string + version string + kind string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "valid GVK", + args: args{group: "kyma-project.io", version: "v1alpha1", kind: "Module"}, + wantErr: false, + }, + { + name: "invalid GVK when group empty", + args: args{version: "v1alpha1", kind: "Module"}, + wantErr: true, + }, + { + name: "invalid GVK when version empty", + args: args{group: "kyma-project.io", kind: "Module"}, + wantErr: true, + }, + { + name: "invalid GVK when kind empty", + args: args{group: "kyma-project.io", version: "v1alpha1"}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validation.ValidateGvk(tt.args.group, tt.args.version, tt.args.kind); (err != nil) != tt.wantErr { + t.Errorf("ValidateGvk() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + func TestValidateMapEntries(t *testing.T) { tests := []struct { name string diff --git a/internal/service/contentprovider/moduleconfig.go b/internal/service/contentprovider/moduleconfig.go index 1e98df8e..3a368408 100644 --- a/internal/service/contentprovider/moduleconfig.go +++ b/internal/service/contentprovider/moduleconfig.go @@ -74,24 +74,57 @@ 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"` - Manifest string `yaml:"manifest" comment:"required, relative path or remote URL to the manifests"` - Repository string `yaml:"repository" comment:"required, link to the repository"` - Documentation string `yaml:"documentation" comment:"required, link to documentation"` - Icons Icons `yaml:"icons,omitempty" comment:"required, list of icons to represent the module in the UI"` - 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 Resources `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"` + Repository string `yaml:"repository" comment:"required, link to the repository"` + Documentation string `yaml:"documentation" comment:"required, link to documentation"` + Icons Icons `yaml:"icons,omitempty" comment:"required, list of icons to represent the module in the UI"` + 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"` + 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"` + AssociatedResources []*metav1.GroupVersionKind `yaml:"associatedResources" comment:"optional, GVK of the resources which are associated with the module and have to be deleted with module deletion"` + 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 Resources `yaml:"resources,omitempty" comment:"optional, additional resources of the ModuleTemplate that may be fetched"` +} + +type Icons map[string]string + +type icon struct { + Name string `yaml:"name"` + Link string `yaml:"link"` +} + +func (i *Icons) UnmarshalYAML(unmarshal func(interface{}) error) error { + icons := []icon{} + if err := unmarshal(&icons); err != nil { + return err + } + + *i = make(map[string]string) + for _, icon := range icons { + (*i)[icon.Name] = icon.Link + } + + if len(icons) > len(*i) { + return ErrDuplicateResourceNames + } + + return nil +} + +func (i Icons) MarshalYAML() (interface{}, error) { + icons := []icon{} + for name, link := range i { + icons = append(icons, icon{Name: name, Link: link}) + } + return icons, nil } type Manager struct { @@ -100,20 +133,13 @@ type Manager struct { metav1.GroupVersionKind `yaml:",inline" comment:"required, the GVK of the manager"` } -type resource struct { - Name string `yaml:"name"` - Link string `yaml:"link"` -} +type Resources map[string]string -type icon struct { +type resource struct { Name string `yaml:"name"` Link string `yaml:"link"` } -type Resources map[string]string - -type Icons map[string]string - func (rm *Resources) UnmarshalYAML(unmarshal func(interface{}) error) error { resources := []resource{} if err := unmarshal(&resources); err != nil { @@ -139,29 +165,3 @@ func (rm Resources) MarshalYAML() (interface{}, error) { } return resources, nil } - -func (i *Icons) UnmarshalYAML(unmarshal func(interface{}) error) error { - icons := []icon{} - if err := unmarshal(&icons); err != nil { - return err - } - - *i = make(map[string]string) - for _, icon := range icons { - (*i)[icon.Name] = icon.Link - } - - if len(icons) > len(*i) { - return ErrDuplicateResourceNames - } - - return nil -} - -func (i Icons) MarshalYAML() (interface{}, error) { - icons := []icon{} - for name, link := range i { - icons = append(icons, icon{Name: name, Link: link}) - } - return icons, nil -} diff --git a/internal/service/moduleconfig/generator/moduleconfig_generator_test.go b/internal/service/moduleconfig/generator/moduleconfig_generator_test.go index 1ee8ed2c..b91e1e23 100644 --- a/internal/service/moduleconfig/generator/moduleconfig_generator_test.go +++ b/internal/service/moduleconfig/generator/moduleconfig_generator_test.go @@ -118,19 +118,25 @@ 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, - 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"}, + Name: "module-name", + Version: "0.0.1", + Channel: "regular", + Manifest: "path/to/manifests", + Mandatory: false, + DefaultCR: "path/to/defaultCR", + Namespace: "kcp-system", + Security: "path/to/securityConfig", + Internal: false, + Beta: false, + Labels: map[string]string{"label1": "value1"}, + Annotations: map[string]string{"annotation1": "value1"}, + AssociatedResources: []*metav1.GroupVersionKind{ + { + Group: "networking.istio.io", + Version: "v1alpha3", + Kind: "Gateway", + }, + }, Manager: &contentprovider.Manager{ Name: "manager-name", Namespace: "manager-namespace", @@ -140,6 +146,7 @@ func (*fileExistsStub) ReadFile(_ string) ([]byte, error) { Kind: "Deployment", }, }, + Resources: contentprovider.ResourcesMap{}, } return yaml.Marshal(moduleConfig) diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader.go b/internal/service/moduleconfig/reader/moduleconfig_reader.go index 4cc53d94..2686b5f7 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader.go @@ -4,6 +4,7 @@ import ( "fmt" "gopkg.in/yaml.v3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" commonerrors "github.com/kyma-project/modulectl/internal/common/errors" "github.com/kyma-project/modulectl/internal/common/validation" @@ -89,6 +90,10 @@ func ValidateModuleConfig(moduleConfig *contentprovider.ModuleConfig) error { } } + if err := ValidateAssociatedResources(moduleConfig.AssociatedResources); err != nil { + return fmt.Errorf("failed to validate associated resources: %w", err) + } + if err := ValidateManager(moduleConfig.Manager); err != nil { return fmt.Errorf("failed to validate manager: %w", err) } @@ -96,6 +101,15 @@ func ValidateModuleConfig(moduleConfig *contentprovider.ModuleConfig) error { return nil } +func ValidateAssociatedResources(resources []*metav1.GroupVersionKind) error { + for _, resource := range resources { + if err := validation.ValidateGvk(resource.Group, resource.Version, resource.Kind); err != nil { + return fmt.Errorf("GVK is invalid: %w", err) + } + } + return nil +} + func ValidateManager(manager *contentprovider.Manager) error { if manager == nil { return nil @@ -105,16 +119,8 @@ func ValidateManager(manager *contentprovider.Manager) error { return fmt.Errorf("name must not be empty: %w", commonerrors.ErrInvalidOption) } - if manager.Kind == "" { - return fmt.Errorf("kind must not be empty: %w", commonerrors.ErrInvalidOption) - } - - if manager.Group == "" { - return fmt.Errorf("group must not be empty: %w", commonerrors.ErrInvalidOption) - } - - if manager.Version == "" { - return fmt.Errorf("version must not be empty: %w", commonerrors.ErrInvalidOption) + if err := validation.ValidateGvk(manager.Group, manager.Version, manager.Kind); err != nil { + return fmt.Errorf("GVK is invalid: %w", err) } if manager.Namespace != "" { diff --git a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go index 1367edb3..d31eedcf 100644 --- a/internal/service/moduleconfig/reader/moduleconfig_reader_test.go +++ b/internal/service/moduleconfig/reader/moduleconfig_reader_test.go @@ -34,7 +34,6 @@ func Test_ParseModuleConfig_Returns_CorrectModuleConfig(t *testing.T) { require.Equal(t, "regular", result.Channel) require.Equal(t, "https://example.com/path/to/manifests", result.Manifest) require.Equal(t, "https://example.com/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) require.Equal(t, "path/to/securityConfig", result.Security) @@ -42,6 +41,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, "networking.istio.io", result.AssociatedResources[0].Group) + require.Equal(t, "v1alpha3", result.AssociatedResources[0].Version) + require.Equal(t, "Gateway", result.AssociatedResources[0].Kind) require.Equal(t, contentprovider.Resources{ "rawManifest": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", }, result.Resources) @@ -121,7 +123,8 @@ func Test_ValidateModuleConfig(t *testing.T) { Namespace: "kcp-system", Manifest: "", }, - expectedError: fmt.Errorf("failed to validate manifest: %w: must not be empty", commonerrors.ErrInvalidOption), + expectedError: fmt.Errorf("failed to validate manifest: %w: must not be empty", + commonerrors.ErrInvalidOption), }, { name: "invalid module resources - not a URL", @@ -135,7 +138,8 @@ func Test_ValidateModuleConfig(t *testing.T) { "key": "%% not a URL", }, }, - expectedError: fmt.Errorf("failed to validate resources: failed to validate link: %w: '%%%% not a URL' is not a valid URL", commonerrors.ErrInvalidOption), + expectedError: fmt.Errorf("failed to validate resources: failed to validate link: %w: '%%%% not a URL' is not a valid URL", + commonerrors.ErrInvalidOption), }, { name: "invalid module resources - empty name", @@ -149,7 +153,8 @@ 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: %w: 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", @@ -163,7 +168,8 @@ func Test_ValidateModuleConfig(t *testing.T) { "name": "", }, }, - expectedError: fmt.Errorf("failed to validate resources: %w: link must not be empty", commonerrors.ErrInvalidOption), + expectedError: fmt.Errorf("failed to validate resources: %w: link must not be empty", + commonerrors.ErrInvalidOption), }, { name: "manifest file path", @@ -174,7 +180,8 @@ func Test_ValidateModuleConfig(t *testing.T) { Namespace: "kcp-system", Manifest: "./test", }, - expectedError: fmt.Errorf("failed to validate manifest: %w: './test' is not using https scheme", commonerrors.ErrInvalidOption), + expectedError: fmt.Errorf("failed to validate manifest: %w: './test' is not using https scheme", + commonerrors.ErrInvalidOption), }, { name: "default CR file path", @@ -186,7 +193,8 @@ func Test_ValidateModuleConfig(t *testing.T) { Manifest: "https://example.com/test", DefaultCR: "/test", }, - expectedError: fmt.Errorf("failed to validate default CR: %w: '/test' is not using https scheme", commonerrors.ErrInvalidOption), + expectedError: fmt.Errorf("failed to validate default CR: %w: '/test' is not using https scheme", + commonerrors.ErrInvalidOption), }, } for _, test := range tests { @@ -299,6 +307,58 @@ func Test_ValidateManager(t *testing.T) { } } +func Test_ValidateAssociatedResources(t *testing.T) { + tests := []struct { + name string + resources []*metav1.GroupVersionKind + wantErr bool + }{ + { + name: "pass on empty resources", + resources: []*metav1.GroupVersionKind{}, + wantErr: false, + }, + { + name: "pass when all resources are valid", + resources: []*metav1.GroupVersionKind{ + { + Group: "networking.istio.io", + Version: "v1alpha3", + Kind: "Gateway", + }, + { + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + }, + wantErr: false, + }, + { + name: "fail when even one resources is invalid", + resources: []*metav1.GroupVersionKind{ + { + Group: "networking.istio.io", + Version: "v1alpha3", + Kind: "Gateway", + }, + { + Group: "apps", + Kind: "Deployment", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := moduleconfigreader.ValidateAssociatedResources(tt.resources); (err != nil) != tt.wantErr { + t.Errorf("ValidateAssociatedResources() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + // Test Stubs type fileExistsStub struct{} @@ -308,21 +368,24 @@ func (*fileExistsStub) FileExists(_ string) (bool, error) { } var expectedReturnedModuleConfig = contentprovider.ModuleConfig{ - Name: "github.com/module-name", - Version: "0.0.1", - Channel: "regular", - Manifest: "https://example.com/path/to/manifests", - Mandatory: false, - DefaultCR: "https://example.com/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.Resources{ - "rawManifest": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + Name: "github.com/module-name", + Version: "0.0.1", + Channel: "regular", + Manifest: "https://example.com/path/to/manifests", + Mandatory: false, + DefaultCR: "https://example.com/path/to/defaultCR", + Namespace: "kcp-system", + Security: "path/to/securityConfig", + Internal: false, + Beta: false, + Labels: map[string]string{"label1": "value1"}, + Annotations: map[string]string{"annotation1": "value1"}, + AssociatedResources: []*metav1.GroupVersionKind{ + { + Group: "networking.istio.io", + Version: "v1alpha3", + Kind: "Gateway", + }, }, Manager: &contentprovider.Manager{ Name: "manager-name", @@ -333,6 +396,9 @@ var expectedReturnedModuleConfig = contentprovider.ModuleConfig{ Kind: "Deployment", }, }, + Resources: contentprovider.Resources{ + "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 4006c6b2..c247e4cf 100644 --- a/internal/service/templategenerator/templategenerator.go +++ b/internal/service/templategenerator/templategenerator.go @@ -8,6 +8,7 @@ import ( "text/template" "github.com/kyma-project/lifecycle-manager/api/shared" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "ocm.software/ocm/api/oci" "ocm.software/ocm/api/ocm/compdesc" "sigs.k8s.io/yaml" @@ -60,6 +61,26 @@ metadata: spec: channel: {{.Channel}} mandatory: {{.Mandatory}} +{{- with .Info}} + info: + repository: {{.Repository}} + documentation: {{.Documentation}} + {{- with .Icons}} + icons: + {{- range $key, $value := . }} + - name: {{ $key }} + link: {{ $value }} + {{- end}} + {{- end}} +{{- end}} +{{- with .AssociatedResources}} + associatedResources: + {{- range .}} + - group: {{.Group}} + version: {{.Version}} + kind: {{.Kind}} + {{- end}} +{{- end}} {{- with .Data}} data: {{. | indent 4}} @@ -83,35 +104,24 @@ spec: link: {{ $value }} {{- end}} {{- end}} -{{- with .Info}} - info: - repository: {{.Repository}} - documentation: {{.Documentation}} - {{- with .Icons}} - icons: - {{- range $key, $value := . }} - - name: {{ $key }} - link: {{ $value }} - {{- end}} - {{- end}} -{{- end}} ` ) type moduleTemplateData struct { - ResourceName string - Namespace string - Descriptor compdesc.ComponentDescriptorVersion - Channel string - Repository string - Documentation string - Icons contentprovider.Icons - Labels map[string]string - Annotations map[string]string - Mandatory bool - Data string - Resources contentprovider.Resources - Manager *contentprovider.Manager + ResourceName string + Namespace string + Descriptor compdesc.ComponentDescriptorVersion + Channel string + Repository string + Documentation string + Icons contentprovider.Icons + Labels map[string]string + Annotations map[string]string + Mandatory bool + Data string + AssociatedResources []*metav1.GroupVersionKind + Resources contentprovider.Resources + Manager *contentprovider.Manager } func (s *Service) GenerateModuleTemplate( @@ -137,9 +147,7 @@ func (s *Service) GenerateModuleTemplate( } shortName := trimShortNameFromRef(ref) labels[shared.ModuleName] = shortName - if moduleConfig.ResourceName == "" { - moduleConfig.ResourceName = shortName + "-" + moduleConfig.Channel - } + moduleTemplateName := shortName + "-" + moduleConfig.Version moduleTemplate, err := template.New("moduleTemplate").Funcs(template.FuncMap{ "yaml": yaml.Marshal, @@ -155,16 +163,17 @@ func (s *Service) GenerateModuleTemplate( } mtData := moduleTemplateData{ - ResourceName: moduleConfig.ResourceName, - Namespace: moduleConfig.Namespace, - Descriptor: cva, - Channel: moduleConfig.Channel, - Repository: moduleConfig.Repository, - Documentation: moduleConfig.Documentation, - Icons: moduleConfig.Icons, - Labels: labels, - Annotations: annotations, - Mandatory: moduleConfig.Mandatory, + ResourceName: moduleTemplateName, + Namespace: moduleConfig.Namespace, + Descriptor: cva, + Channel: moduleConfig.Channel, + Repository: moduleConfig.Repository, + Documentation: moduleConfig.Documentation, + Icons: moduleConfig.Icons, + Labels: labels, + Annotations: annotations, + Mandatory: moduleConfig.Mandatory, + AssociatedResources: moduleConfig.AssociatedResources, Resources: contentprovider.Resources{ "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 59eb0818..d212cd74 100644 --- a/internal/service/templategenerator/templategenerator_test.go +++ b/internal/service/templategenerator/templategenerator_test.go @@ -43,14 +43,14 @@ func TestGenerateModuleTemplate_Success(t *testing.T) { svc, _ := templategenerator.NewService(mockFS) moduleConfig := &contentprovider.ModuleConfig{ - ResourceName: "test-resource", - Namespace: "default", - Channel: "stable", - Labels: map[string]string{"key": "value"}, - Annotations: map[string]string{"annotation": "value"}, - Mandatory: true, - Manifest: "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", - Resources: contentprovider.Resources{"someResource": "https://some.other/location/template-operator.yaml"}, + Namespace: "default", + Version: "1.0.0", + Channel: "stable", + Labels: map[string]string{"key": "value"}, + Annotations: map[string]string{"annotation": "value"}, + Mandatory: true, + Manifest: "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml", + Resources: contentprovider.Resources{"someResource": "https://some.other/location/template-operator.yaml"}, } descriptor := testutils.CreateComponentDescriptor("example.com/component", "1.0.0") data := []byte("test-data") @@ -59,7 +59,7 @@ func TestGenerateModuleTemplate_Success(t *testing.T) { require.NoError(t, err) require.Equal(t, "output.yaml", mockFS.path) - require.Contains(t, mockFS.writtenTemplate, "test-resource") + require.Contains(t, mockFS.writtenTemplate, "component-1.0.0") require.Contains(t, mockFS.writtenTemplate, "default") require.Contains(t, mockFS.writtenTemplate, "stable") require.Contains(t, mockFS.writtenTemplate, "test-data") @@ -67,7 +67,8 @@ func TestGenerateModuleTemplate_Success(t *testing.T) { 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") + 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) { @@ -87,7 +88,43 @@ func TestGenerateModuleTemplate_Success_With_Overwritten_RawManifest(t *testing. 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") + require.NotContains(t, mockFS.writtenTemplate, + "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml") +} + +func TestGenerateModuleTemplateWithAssociatedResources_Success(t *testing.T) { + mockFS := &mockFileSystem{} + svc, _ := templategenerator.NewService(mockFS) + + moduleConfig := &contentprovider.ModuleConfig{ + Namespace: "default", + Channel: "stable", + Labels: map[string]string{"key": "value"}, + Annotations: map[string]string{"annotation": "value"}, + Mandatory: true, + AssociatedResources: []*metav1.GroupVersionKind{ + { + Group: "networking.istio.io", + Version: "v1alpha3", + Kind: "Gateway", + }, + }, + } + 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, "default") + 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, "associatedResources") + require.Contains(t, mockFS.writtenTemplate, "networking.istio.io") + require.Contains(t, mockFS.writtenTemplate, "v1alpha3") + require.Contains(t, mockFS.writtenTemplate, "Gateway") } func TestGenerateModuleTemplateWithManager_Success(t *testing.T) { @@ -95,12 +132,12 @@ func TestGenerateModuleTemplateWithManager_Success(t *testing.T) { svc, _ := templategenerator.NewService(mockFS) moduleConfig := &contentprovider.ModuleConfig{ - ResourceName: "test-resource", - Namespace: "default", - Channel: "stable", - Labels: map[string]string{"key": "value"}, - Annotations: map[string]string{"annotation": "value"}, - Mandatory: true, + Namespace: "default", + Channel: "stable", + Version: "1.0.0", + Labels: map[string]string{"key": "value"}, + Annotations: map[string]string{"annotation": "value"}, + Mandatory: true, Manager: &contentprovider.Manager{ Name: "manager-name", Namespace: "manager-ns", @@ -118,7 +155,7 @@ func TestGenerateModuleTemplateWithManager_Success(t *testing.T) { require.NoError(t, err) require.Equal(t, "output.yaml", mockFS.path) - require.Contains(t, mockFS.writtenTemplate, "test-resource") + require.Contains(t, mockFS.writtenTemplate, "component-1.0.0") require.Contains(t, mockFS.writtenTemplate, "default") require.Contains(t, mockFS.writtenTemplate, "stable") require.Contains(t, mockFS.writtenTemplate, "test-data") @@ -136,12 +173,12 @@ func TestGenerateModuleTemplateWithManagerWithoutNamespace_Success(t *testing.T) svc, _ := templategenerator.NewService(mockFS) moduleConfig := &contentprovider.ModuleConfig{ - ResourceName: "test-resource", - Namespace: "default", - Channel: "stable", - Labels: map[string]string{"key": "value"}, - Annotations: map[string]string{"annotation": "value"}, - Mandatory: true, + Namespace: "default", + Channel: "stable", + Version: "1.0.0", + Labels: map[string]string{"key": "value"}, + Annotations: map[string]string{"annotation": "value"}, + Mandatory: true, Manager: &contentprovider.Manager{ Name: "manager-name", GroupVersionKind: metav1.GroupVersionKind{ @@ -158,7 +195,7 @@ func TestGenerateModuleTemplateWithManagerWithoutNamespace_Success(t *testing.T) require.NoError(t, err) require.Equal(t, "output.yaml", mockFS.path) - require.Contains(t, mockFS.writtenTemplate, "test-resource") + require.Contains(t, mockFS.writtenTemplate, "component-1.0.0") require.Contains(t, mockFS.writtenTemplate, "default") require.Contains(t, mockFS.writtenTemplate, "stable") require.Contains(t, mockFS.writtenTemplate, "test-data") diff --git a/scaffold-create-config.yaml b/scaffold-create-config.yaml index cba04664..e74bf28e 100644 --- a/scaffold-create-config.yaml +++ b/scaffold-create-config.yaml @@ -4,7 +4,6 @@ channel: "regular" # required, channel that should be used in the ModuleTemplate manifest: "manifest.yaml" # required, relative path or remote URL to the manifests # mandatory: false # optional, default=false, indicates whether the module is mandatory to be installed on all clusters # defaultCR: "" # optional, relative path or remote URL to a YAML file containing the default CR for the module -# resourceName: "" # optional, default={name}-{channel}, when channel is 'none', the default is {name}-{version}, the name for the ModuleTemplate that will be created # namespace: "" # optional, default=kcp-system, the namespace where the ModuleTemplate will be deployed # security: "" # optional, name of the security scanners config file # internal: false # optional, default=false, determines whether the ModuleTemplate should have the internal flag or not diff --git a/tests/e2e/create/create_suite_test.go b/tests/e2e/create/create_suite_test.go index f382a419..4c19f835 100644 --- a/tests/e2e/create/create_suite_test.go +++ b/tests/e2e/create/create_suite_test.go @@ -22,7 +22,6 @@ const ( 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" @@ -34,16 +33,17 @@ const ( manifestFileref = invalidConfigs + "manifest-fileref.yaml" defaultCRFileref = invalidConfigs + "defaultcr-fileref.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" - withManagerConfig = validConfigs + "with-manager.yaml" - withNoNamespaceManagerConfig = validConfigs + "with-manager-no-namespace.yaml" + validConfigs = testdataDir + "valid/" + minimalConfig = validConfigs + "minimal.yaml" + withAnnotationsConfig = validConfigs + "with-annotations.yaml" + withDefaultCrConfig = validConfigs + "with-defaultcr.yaml" + withSecurityConfig = validConfigs + "with-security.yaml" + withMandatoryConfig = validConfigs + "with-mandatory.yaml" + withAssociatedResourcesConfig = validConfigs + "with-associated-resources.yaml" + withResources = validConfigs + "with-resources.yaml" + withResourcesOverwrite = validConfigs + "with-resources-overwrite.yaml" + withManagerConfig = validConfigs + "with-manager.yaml" + withNoNamespaceManagerConfig = validConfigs + "with-manager-no-namespace.yaml" ociRegistry = "http://k3d-oci.localhost:5001" templateOutputPath = "/tmp/template.yaml" diff --git a/tests/e2e/create/create_test.go b/tests/e2e/create/create_test.go index d2971e1c..a5a93b9f 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" @@ -107,34 +107,6 @@ var _ = Describe("Test 'create' command", Ordered, func() { }) }) - Context("Given 'modulectl create' command", func() { - var cmd createCmd - It("When invoked with '--config-file' using file with missing info", func() { - cmd = createCmd{ - moduleConfigFile: missingInfoConfig, - } - }) - It("Then the command should fail", func() { - err := cmd.execute() - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("invalid Option: opts.ModuleInfo must not be empty")) - }) - }) - - Context("Given 'modulectl create' command", func() { - var cmd createCmd - It("When invoked with empty resource name", func() { - cmd = createCmd{ - moduleConfigFile: emptyResourceName, - } - }) - It("Then the command should fail", func() { - err := cmd.execute() - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("failed to parse module config: failed to validate module config: failed to validate resources: invalid Option: name must not be empty")) - }) - }) - Context("Given 'modulectl create' command", func() { var cmd createCmd It("When invoked with non https resource", func() { @@ -231,6 +203,14 @@ var _ = Describe("Test 'create' command", Ordered, func() { descriptor := getDescriptor(template) Expect(descriptor).ToNot(BeNil()) Expect(descriptor.SchemaVersion()).To(Equal(v2.SchemaVersion)) + Expect(template.Name).To(Equal("template-operator-1.0.0")) + + By("And spec.info should be correct") + Expect(template.Spec.Info.Repository).To(Equal("https://github.com/kyma-project/template-operator")) + Expect(template.Spec.Info.Documentation).To(Equal("https://github.com/kyma-project/template-operator/blob/main/README.md")) + Expect(template.Spec.Info.Icons).To(HaveLen(1)) + Expect(template.Spec.Info.Icons[0].Name).To(Equal("module-icon")) + Expect(template.Spec.Info.Icons[0].Link).To(Equal("https://github.com/kyma-project/template-operator/blob/main/docs/assets/logo.png")) By("And annotations should be correct") annotations := template.Annotations @@ -267,12 +247,9 @@ var _ = Describe("Test 'create' command", Ordered, func() { By("And spec.mandatory should be false") Expect(template.Spec.Mandatory).To(BeFalse()) - By("And spec.info should be correct") - Expect(template.Spec.Info.Repository).To(Equal("https://github.com/kyma-project/template-operator")) - Expect(template.Spec.Info.Documentation).To(Equal("https://github.com/kyma-project/template-operator/blob/main/README.md")) - Expect(template.Spec.Info.Icons).To(HaveLen(1)) - Expect(template.Spec.Info.Icons[0].Name).To(Equal("module-icon")) - Expect(template.Spec.Info.Icons[0].Link).To(Equal("https://github.com/kyma-project/template-operator/blob/main/docs/assets/logo.png")) + By("And spec.associatedResources should be empty") + Expect(template.Spec.AssociatedResources).To(BeEmpty()) + By("And spec.manager should be nil") Expect(template.Spec.Manager).To(BeNil()) @@ -319,6 +296,7 @@ var _ = Describe("Test 'create' command", Ordered, func() { Expect(err).ToNot(HaveOccurred()) descriptor := getDescriptor(template) Expect(descriptor).ToNot(BeNil()) + Expect(template.Name).To(Equal("template-operator-1.0.1")) By("And new annotation should be correctly added") annotations := template.Annotations @@ -353,6 +331,7 @@ var _ = Describe("Test 'create' command", Ordered, func() { Expect(err).ToNot(HaveOccurred()) descriptor := getDescriptor(template) Expect(descriptor).ToNot(BeNil()) + Expect(template.Name).To(Equal("template-operator-1.0.2")) By("And annotation should have correct version") annotations := template.Annotations @@ -400,6 +379,7 @@ var _ = Describe("Test 'create' command", Ordered, func() { Expect(err).ToNot(HaveOccurred()) descriptor := getDescriptor(template) Expect(descriptor).ToNot(BeNil()) + Expect(template.Name).To(Equal("template-operator-1.0.3")) By("And descriptor.component.resources should be correct") Expect(descriptor.Resources).To(HaveLen(2)) @@ -475,6 +455,7 @@ var _ = Describe("Test 'create' command", Ordered, func() { Expect(err).ToNot(HaveOccurred()) descriptor := getDescriptor(template) Expect(descriptor).ToNot(BeNil()) + Expect(template.Name).To(Equal("template-operator-1.0.4")) By("And annotation should have correct version") annotations := template.Annotations @@ -506,6 +487,7 @@ var _ = Describe("Test 'create' command", Ordered, func() { Expect(err).ToNot(HaveOccurred()) descriptor := getDescriptor(template) Expect(descriptor).ToNot(BeNil()) + Expect(template.Name).To(Equal("template-operator-1.0.5")) By("And annotation should have correct version") annotations := template.Annotations @@ -544,6 +526,7 @@ var _ = Describe("Test 'create' command", Ordered, func() { Expect(err).ToNot(HaveOccurred()) descriptor := getDescriptor(template) Expect(descriptor).ToNot(BeNil()) + Expect(template.Name).To(Equal("template-operator-1.0.6")) By("And annotation should have correct version") annotations := template.Annotations @@ -560,6 +543,44 @@ var _ = Describe("Test 'create' command", Ordered, func() { }) }) + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with valid module-config containing associatedResources list", func() { + cmd = createCmd{ + moduleConfigFile: withAssociatedResourcesConfig, + registry: ociRegistry, + 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 the expected content", func() { + template, err := readModuleTemplate(templateOutputPath) + Expect(err).ToNot(HaveOccurred()) + descriptor := getDescriptor(template) + Expect(descriptor).ToNot(BeNil()) + + Expect(template.Name).To(Equal("template-operator-1.0.7")) + + By("And annotation should have correct version") + annotations := template.Annotations + Expect(annotations[shared.ModuleVersionAnnotation]).To(Equal("1.0.7")) + + By("And spec.associatedResources should be correct") + resources := template.Spec.AssociatedResources + Expect(resources).ToNot(BeEmpty()) + Expect(len(resources)).To(Equal(1)) + Expect(resources[0].Group).To(Equal("networking.istio.io")) + Expect(resources[0].Version).To(Equal("v1alpha3")) + Expect(resources[0].Kind).To(Equal("Gateway")) + }) + }) + Context("Given 'modulectl create' command", func() { var cmd createCmd It("When invoked with minimal valid module-config containing resources", func() { diff --git a/tests/e2e/create/testdata/moduleconfig/invalid/empty-resource-name.yaml b/tests/e2e/create/testdata/moduleconfig/valid/with-associated-resources.yaml similarity index 59% rename from tests/e2e/create/testdata/moduleconfig/invalid/empty-resource-name.yaml rename to tests/e2e/create/testdata/moduleconfig/valid/with-associated-resources.yaml index 229197a1..1cc75132 100644 --- a/tests/e2e/create/testdata/moduleconfig/invalid/empty-resource-name.yaml +++ b/tests/e2e/create/testdata/moduleconfig/valid/with-associated-resources.yaml @@ -1,12 +1,13 @@ name: kyma-project.io/module/template-operator channel: regular -version: 1.0.0 +version: 1.0.7 manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml repository: https://github.com/kyma-project/template-operator documentation: https://github.com/kyma-project/template-operator/blob/main/README.md icons: -- name: module-icon - link: https://github.com/kyma-project/template-operator/blob/main/docs/assets/logo.png -resources: -- name: "" - link: http://some.other/location/template-operator.yaml + - name: module-icon + link: https://github.com/kyma-project/template-operator/blob/main/docs/assets/logo.png +associatedResources: + - group: networking.istio.io + version: v1alpha3 + kind: Gateway diff --git a/tests/e2e/scaffold/scaffold_suite_test.go b/tests/e2e/scaffold/scaffold_suite_test.go index a666607d..cdf04686 100644 --- a/tests/e2e/scaffold/scaffold_suite_test.go +++ b/tests/e2e/scaffold/scaffold_suite_test.go @@ -47,7 +47,7 @@ func (cmd *scaffoldCmd) execute() error { } if cmd.moduleConfigFileFlag != "" { - args = append(args, "--module-config="+cmd.moduleConfigFileFlag) + args = append(args, "--config-file="+cmd.moduleConfigFileFlag) } if cmd.genDefaultCRFlag != "" { @@ -157,7 +157,6 @@ type moduleConfig struct { 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"` diff --git a/unit-test-coverage.yaml b/unit-test-coverage.yaml index 933014e3..83223ef4 100644 --- a/unit-test-coverage.yaml +++ b/unit-test-coverage.yaml @@ -8,7 +8,7 @@ packages: internal/service/filegenerator/reusefilegenerator: 94 internal/service/fileresolver: 100 internal/service/moduleconfig/generator: 100 - internal/service/moduleconfig/reader: 78 + internal/service/moduleconfig/reader: 76 internal/service/create: 43 internal/service/componentdescriptor: 78 internal/service/templategenerator: 85