Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Generate .spec.resources #73

Merged
merged 22 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions cmd/modulectl/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
33 changes: 33 additions & 0 deletions internal/common/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package validation

import (
"fmt"
"net/url"
"regexp"
"strings"

"github.com/Masterminds/semver/v3"

commonerrors "github.com/kyma-project/modulectl/internal/common/errors"
"github.com/kyma-project/modulectl/internal/service/contentprovider"
)

const (
Expand Down Expand Up @@ -107,6 +109,37 @@ func ValidateNamespace(namespace string) error {
return nil
}

func ValidateResources(resources contentprovider.ResourcesMap) error {
for name, link := range resources {
if name == "" {
return fmt.Errorf("%w: name must not be empty", commonerrors.ErrInvalidOption)
}

if link == "" {
c-pius marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("%w: link must not be empty", commonerrors.ErrInvalidOption)
}

if err := ValidateIsValidHTTPSURL(link); err != nil {
return err
}
}

return nil
}

func ValidateIsValidHTTPSURL(input string) error {
_url, err := url.Parse(input)
if err != nil {
return fmt.Errorf("%w: link %s is not a valid URL", commonerrors.ErrInvalidOption, input)
}

if _url.Scheme != "https" {
return fmt.Errorf("%w: link %s is not using https scheme", commonerrors.ErrInvalidOption, input)
}

return nil
}

func validateSemanticVersion(version string) error {
_, err := semver.StrictNewVersion(strings.TrimSpace(version))
if err != nil {
Expand Down
95 changes: 95 additions & 0 deletions internal/common/validation/validation_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package validation_test

import (
"fmt"
"testing"

"github.com/kyma-project/modulectl/internal/common/validation"
"github.com/kyma-project/modulectl/internal/service/contentprovider"
)

func TestValidateModuleName(t *testing.T) {
Expand Down Expand Up @@ -225,3 +227,96 @@ func TestValidateNamespace(t *testing.T) {
})
}
}

func TestValidateResources(t *testing.T) {
tests := []struct {
name string
resources contentprovider.ResourcesMap
wantErr bool
}{
{
name: "valid resources",
resources: contentprovider.ResourcesMap{
"first": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml",
"second": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml",
},
wantErr: false,
},
{
name: "empty name",
resources: contentprovider.ResourcesMap{
"": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml",
},
wantErr: true,
},
{
name: "empty link",
resources: contentprovider.ResourcesMap{
"first": "",
},
wantErr: true,
},
{
name: "non-https schema",
resources: contentprovider.ResourcesMap{
"first": "http://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validation.ValidateResources(tt.resources); (err != nil) != tt.wantErr {
fmt.Println(err.Error())
t.Errorf("ValidateResources() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestValidateIsValidHttpsUrl(t *testing.T) {
tests := []struct {
name string
url string
wantErr bool
}{
{
name: "valid url",
url: "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml",
wantErr: false,
},
{
name: "invalid url - not using https",
url: "http://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml",
wantErr: true,
},
{
name: "invalid url - usig file scheme",
url: "file:///Users/User/template-operator/releases/download/1.0.1/template-operator.yaml",
wantErr: true,
},
{
name: "invalid url - local path",
url: "./1.0.1/template-operator.yaml",
wantErr: true,
},
{
name: "invalid url",
url: "%% not a valid url",
wantErr: true,
},
{
name: "empty url",
url: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validation.ValidateIsValidHTTPSURL(tt.url); (err != nil) != tt.wantErr {
fmt.Println(err.Error())
t.Errorf("ValidateIsValidUrl() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
77 changes: 57 additions & 20 deletions internal/service/contentprovider/moduleconfig.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package contentprovider

import (
"errors"
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -9,6 +10,8 @@ import (
"github.com/kyma-project/modulectl/internal/common/types"
)

var ErrDuplicateResourceNames = errors.New("resources contain duplicate entries")

type ModuleConfigProvider struct {
yamlConverter ObjectToYAMLConverter
}
Expand All @@ -35,12 +38,12 @@ func (s *ModuleConfigProvider) GetDefaultContent(args types.KeyValueArgs) (strin

func (s *ModuleConfigProvider) getModuleConfig(args types.KeyValueArgs) ModuleConfig {
return ModuleConfig{
Name: args[ArgModuleName],
Version: args[ArgModuleVersion],
Channel: args[ArgModuleChannel],
ManifestPath: args[ArgManifestFile],
Security: args[ArgSecurityConfigFile],
DefaultCRPath: args[ArgDefaultCRFile],
Name: args[ArgModuleName],
Version: args[ArgModuleVersion],
Channel: args[ArgModuleChannel],
Manifest: args[ArgManifestFile],
Security: args[ArgSecurityConfigFile],
DefaultCR: args[ArgDefaultCRFile],
}
}

Expand Down Expand Up @@ -77,18 +80,52 @@ 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"`
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"`
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"`
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 {
Name string `yaml:"name"`
Link string `yaml:"link"`
}

type ResourcesMap map[string]string

func (rm *ResourcesMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
resources := []resource{}
if err := unmarshal(&resources); err != nil {
return err
}

*rm = make(map[string]string)
for _, resource := range resources {
(*rm)[resource.Name] = resource.Link
}

if len(resources) > len(*rm) {
return ErrDuplicateResourceNames
}

return nil
}

func (rm ResourcesMap) MarshalYAML() (interface{}, error) {
resources := []resource{}
for name, link := range rm {
resources = append(resources, resource{Name: name, Link: link})
}
return resources, nil
}
81 changes: 81 additions & 0 deletions internal/service/contentprovider/moduleconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
ruanxin marked this conversation as resolved.
Show resolved Hide resolved

commonerrors "github.com/kyma-project/modulectl/internal/common/errors"
"github.com/kyma-project/modulectl/internal/common/types"
Expand Down Expand Up @@ -134,6 +135,86 @@ func Test_ModuleConfig_GetDefaultContent_ReturnsConvertedContent(t *testing.T) {
assert.Equal(t, mcConvertedContent, result)
}

func Test_ModuleConfig_Unmarshall_Resources_Success(t *testing.T) {
moduleConfigData := `
resources:
- name: resource1
link: https://example.com/resource1
- name: resource2
link: https://example.com/resource2
`

moduleConfig := &contentprovider.ModuleConfig{}
err := yaml.Unmarshal([]byte(moduleConfigData), moduleConfig)

require.NoError(t, err)
assert.Len(t, moduleConfig.Resources, 2)
assert.Equal(t, "https://example.com/resource1", moduleConfig.Resources["resource1"])
assert.Equal(t, "https://example.com/resource2", moduleConfig.Resources["resource2"])
}

func Test_ModuleConfig_Unmarshall_Resources_Success_Ignoring_Unknown_Fields(t *testing.T) {
moduleConfigData := `
resources:
- name: resource1
link: https://example.com/resource1
unknown: something
`

moduleConfig := &contentprovider.ModuleConfig{}
err := yaml.Unmarshal([]byte(moduleConfigData), moduleConfig)

require.NoError(t, err)
assert.Len(t, moduleConfig.Resources, 1)
assert.Equal(t, "https://example.com/resource1", moduleConfig.Resources["resource1"])
}

func Test_ModuleConfig_Unmarshall_Resources_FailOnDuplicateNames(t *testing.T) {
moduleConfigData := `
resources:
- name: resource1
link: https://example.com/resource1
- name: resource1
link: https://example.com/resource1
`

moduleConfig := &contentprovider.ModuleConfig{}
err := yaml.Unmarshal([]byte(moduleConfigData), moduleConfig)

require.Error(t, err)
assert.Equal(t, "resources contain duplicate entries", err.Error())
}

func Test_ModuleConfig_Marshall_Resources_Success(t *testing.T) {
// parse the expected config
expectedModuleConfigData := `
resources:
- name: resource1
link: https://example.com/resource1
- name: resource2
link: https://example.com/resource2
`
expectedModuleConfig := &contentprovider.ModuleConfig{}
err := yaml.Unmarshal([]byte(expectedModuleConfigData), expectedModuleConfig)
require.NoError(t, err)

// round trip a module config (marshal and unmarshal)
moduleConfig := &contentprovider.ModuleConfig{
Resources: contentprovider.ResourcesMap{
"resource1": "https://example.com/resource1",
"resource2": "https://example.com/resource2",
},
}
marshalledModuleConfigData, err := yaml.Marshal(moduleConfig)
require.NoError(t, err)

roudTrippedModuleConfig := &contentprovider.ModuleConfig{}
err = yaml.Unmarshal(marshalledModuleConfigData, roudTrippedModuleConfig)

require.NoError(t, err)
assert.Equal(t, expectedModuleConfig.Resources, roudTrippedModuleConfig.Resources)
}

// Test Stubs

type mcObjectToYAMLConverterStub struct{}
Expand Down
Loading
Loading