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 13 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
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 @@ -99,6 +101,37 @@ 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 == "" {
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 @@ -179,3 +181,96 @@ 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
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)
}
})
}
}
66 changes: 52 additions & 14 deletions internal/service/contentprovider/moduleconfig.go
Original file line number Diff line number Diff line change
@@ -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("resources contain duplicate entries")

type ModuleConfigProvider struct {
yamlConverter ObjectToYAMLConverter
}
Expand Down Expand Up @@ -36,7 +39,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],
}
Expand Down Expand Up @@ -69,17 +72,52 @@ 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"`
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
c-pius marked this conversation as resolved.
Show resolved Hide resolved
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 {
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
4 changes: 2 additions & 2 deletions internal/service/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading