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: Add support for spec.associatedResources #75

Merged
Show file tree
Hide file tree
Changes from 10 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: 16 additions & 0 deletions internal/common/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,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
}
41 changes: 41 additions & 0 deletions internal/common/validation/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,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 TestValidateResources(t *testing.T) {
tests := []struct {
name string
Expand Down
31 changes: 16 additions & 15 deletions internal/service/contentprovider/moduleconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,22 @@ 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"`
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"`
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"`
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 ResourcesMap `yaml:"resources,omitempty" comment:"optional, additional resources of the ModuleTemplate that may be fetched"`
}

type resource struct {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ func (*fileExistsStub) ReadFile(_ string) ([]byte, error) {
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",
Expand Down
26 changes: 16 additions & 10 deletions internal/service/moduleconfig/reader/moduleconfig_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,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"
Expand Down Expand Up @@ -76,13 +77,26 @@ 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)
}

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
Expand All @@ -92,16 +106,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 != "" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, "networking.istio.io", result.AssociatedResources[0].Group)
require.Equal(t, "v1alpha3", result.AssociatedResources[0].Version)
require.Equal(t, "Gateway", result.AssociatedResources[0].Kind)
require.Equal(t, contentprovider.ResourcesMap{
"rawManifest": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml",
}, result.Resources)
Expand Down Expand Up @@ -321,6 +324,13 @@ var expectedReturnedModuleConfig = contentprovider.ModuleConfig{
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",
},
},
Resources: contentprovider.ResourcesMap{
"rawManifest": "https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml",
},
Expand Down
45 changes: 28 additions & 17 deletions internal/service/templategenerator/templategenerator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -60,6 +61,14 @@ metadata:
spec:
channel: {{.Channel}}
mandatory: {{.Mandatory}}
{{- with .AssociatedResources}}
associatedResources:
{{- range .}}
- group: {{.Group}}
version: {{.Version}}
kind: {{.Kind}}
{{- end}}
{{- end}}
{{- with .Data}}
data:
{{. | indent 4}}
Expand Down Expand Up @@ -87,16 +96,17 @@ spec:
)

type moduleTemplateData struct {
ResourceName string
Namespace string
Descriptor compdesc.ComponentDescriptorVersion
Channel string
Labels map[string]string
Annotations map[string]string
Mandatory bool
Data string
Resources contentprovider.ResourcesMap
Manager *contentprovider.Manager
ResourceName string
Namespace string
Descriptor compdesc.ComponentDescriptorVersion
Channel string
Labels map[string]string
Annotations map[string]string
Mandatory bool
Data string
AssociatedResources []*metav1.GroupVersionKind
Resources contentprovider.ResourcesMap
Manager *contentprovider.Manager
}

func (s *Service) GenerateModuleTemplate(
Expand Down Expand Up @@ -140,13 +150,14 @@ func (s *Service) GenerateModuleTemplate(
}

mtData := moduleTemplateData{
ResourceName: moduleConfig.ResourceName,
Namespace: moduleConfig.Namespace,
Descriptor: cva,
Channel: moduleConfig.Channel,
Labels: labels,
Annotations: annotations,
Mandatory: moduleConfig.Mandatory,
ResourceName: moduleConfig.ResourceName,
Namespace: moduleConfig.Namespace,
Descriptor: cva,
Channel: moduleConfig.Channel,
Labels: labels,
Annotations: annotations,
Mandatory: moduleConfig.Mandatory,
AssociatedResources: moduleConfig.AssociatedResources,
Resources: contentprovider.ResourcesMap{
"rawManifest": moduleConfig.Manifest, // defaults rawManifest to Manifest; may be overwritten by explicitly provided entries
},
Expand Down
37 changes: 37 additions & 0 deletions internal/service/templategenerator/templategenerator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,43 @@ func TestGenerateModuleTemplate_Success_With_Overwritten_RawManifest(t *testing.
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{
ResourceName: "test-resource",
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, "test-resource")
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) {
mockFS := &mockFileSystem{}
svc, _ := templategenerator.NewService(mockFS)
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/create/create_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const (
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"
Expand Down
40 changes: 40 additions & 0 deletions tests/e2e/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ var _ = Describe("Test 'create' command", Ordered, func() {
By("And spec.mandatory should be false")
Expect(template.Spec.Mandatory).To(BeFalse())

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())

Expand Down Expand Up @@ -539,6 +542,43 @@ var _ = Describe("Test 'create' command", Ordered, func() {
Expect(manager.Kind).To(Equal("Deployment"))
})
})


Context("Given 'modulectl create' command", func() {
var cmd createCmd
It("When invoked with valid module-config containing associatedResources list", func() {
cmd = createCmd{
moduleConfigFile: withAssociatedResourcesConfig,
registry: ociRegistry,
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())

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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: kyma-project.io/module/template-operator
channel: regular
version: 1.0.7
manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml
associatedResources:
- group: networking.istio.io
version: v1alpha3
kind: Gateway
Loading