From 65ebdbe777a5e9c456f2819f1b795e7acfdc54b1 Mon Sep 17 00:00:00 2001 From: Benjamin Lindner <50365642+lindnerby@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:12:23 +0200 Subject: [PATCH] feat: Add E2E Test Suite for module create (#47) * add workflow and tests * fmt * test * test * test * revert renaming fail * renaming * add unit tests for scaffold options Validate function * fix yaml unmarshal of module template * add new ginkgo format create e2e tests * lint * fix linter but keep todo to fail * fix unit tests * use build tags for e2e tests * test new e2e structure against kyma bin * stash * migrate all e2e tests * clean-up * clean-up * adapt some review issues * Add registryUrl != "" check * adapt testcases to new modulectl api * clean-up * Revert "Add registryUrl != "" check" This reverts commit f26ab1ee6f8065547c6b26a4519a4baf7d290082. * Fix E2E tests * Add test case for missing security (still not passing) * add more testdata * use new testdata * clean-up * add testcases for different valid module-configs * bump mandatory version * move consts * fix tests * fix tests * fix tests * fix merge diff * new line * fix e2e expected err * adapt coverage * retrigger jobs --------- Co-authored-by: Badr, Nesma --- .github/workflows/build.yaml | 11 +- .github/workflows/test-e2e-create.yml | 40 ++ ...ate-scaffold.yml => test-e2e-scaffold.yml} | 17 +- ...st-coverage.yml => test-unit-coverage.yml} | 0 .../{unit-test.yaml => test-unit.yaml} | 0 .gitignore | 3 + .golangci.yaml | 9 +- cmd/modulectl/create/cmd.go | 12 +- cmd/modulectl/create/cmd_test.go | 6 +- cmd/modulectl/scaffold/cmd.go | 12 +- cmd/modulectl/scaffold/cmd_test.go | 6 +- go.mod | 4 + go.sum | 7 + internal/service/create/create.go | 2 +- internal/service/create/create_test.go | 10 +- internal/service/scaffold/options.go | 2 +- internal/service/scaffold/options_test.go | 188 ++++++++ internal/service/scaffold/scaffold.go | 4 +- internal/service/scaffold/scaffold_test.go | 221 +-------- tests/e2e/Makefile | 7 +- tests/e2e/create/create_suite_test.go | 84 ++++ tests/e2e/create/create_test.go | 456 ++++++++++++++++++ .../moduleconfig/invalid/missing-channel.yaml | 3 + .../invalid/missing-manifest.yaml | 3 + .../moduleconfig/invalid/missing-name.yaml | 3 + .../moduleconfig/invalid/missing-version.yaml | 3 + .../testdata/moduleconfig/valid/minimal.yaml | 4 + .../moduleconfig/valid/with-annotations.yaml | 6 + .../moduleconfig/valid/with-defaultcr.yaml | 5 + .../moduleconfig/valid/with-mandatory.yaml | 5 + .../moduleconfig/valid/with-security.yaml | 5 + tests/e2e/scaffold/scaffold_suite_test.go | 158 +++++- tests/e2e/scaffold/scaffold_test.go | 156 +----- unit-test-coverage.yaml | 2 +- 34 files changed, 1043 insertions(+), 411 deletions(-) create mode 100644 .github/workflows/test-e2e-create.yml rename .github/workflows/{e2e-test-create-scaffold.yml => test-e2e-scaffold.yml} (62%) rename .github/workflows/{verify-unit-test-coverage.yml => test-unit-coverage.yml} (100%) rename .github/workflows/{unit-test.yaml => test-unit.yaml} (100%) create mode 100644 internal/service/scaffold/options_test.go create mode 100644 tests/e2e/create/create_suite_test.go create mode 100644 tests/e2e/create/create_test.go create mode 100644 tests/e2e/create/testdata/moduleconfig/invalid/missing-channel.yaml create mode 100644 tests/e2e/create/testdata/moduleconfig/invalid/missing-manifest.yaml create mode 100644 tests/e2e/create/testdata/moduleconfig/invalid/missing-name.yaml create mode 100644 tests/e2e/create/testdata/moduleconfig/invalid/missing-version.yaml create mode 100644 tests/e2e/create/testdata/moduleconfig/valid/minimal.yaml create mode 100644 tests/e2e/create/testdata/moduleconfig/valid/with-annotations.yaml create mode 100644 tests/e2e/create/testdata/moduleconfig/valid/with-defaultcr.yaml create mode 100644 tests/e2e/create/testdata/moduleconfig/valid/with-mandatory.yaml create mode 100644 tests/e2e/create/testdata/moduleconfig/valid/with-security.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4c002922..e82b1bf7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,4 +1,4 @@ -name: Build modulectl +name: Build on: pull_request: branches: @@ -6,16 +6,15 @@ on: - 'release-**' workflow_dispatch: jobs: - build: - name: Build modulectl + build-modulectl: runs-on: ubuntu-latest steps: - - name: Checkout modulectl + - name: Checkout uses: actions/checkout@v4 - - name: Set up Go + - name: Go setup uses: actions/setup-go@v4 with: go-version-file: 'go.mod' cache-dependency-path: 'go.sum' - - name: Run CLI Build + - name: "Run 'make build'" run: make build diff --git a/.github/workflows/test-e2e-create.yml b/.github/workflows/test-e2e-create.yml new file mode 100644 index 00000000..58701af8 --- /dev/null +++ b/.github/workflows/test-e2e-create.yml @@ -0,0 +1,40 @@ +name: E2E test - create command +on: + push: + branches: + - main + - 'release-**' + pull_request: + branches: + - main + - 'release-**' +jobs: + test-create-cmd: + runs-on: ubuntu-latest + env: + K3D_VERSION: v5.4.7 + MODULE_TEMPLATE_VERSION: 1.0.0 + OCI_REPOSITORY_URL: http://k3d-oci.localhost:5001 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Go setup + uses: actions/setup-go@v4 + with: + go-version-file: 'go.mod' + cache-dependency-path: 'go.sum' + - name: Build + run: | + make build-linux + chmod +x ./bin/modulectl-linux + ls -la ./bin + mv ./bin/modulectl-linux /usr/local/bin/modulectl + timeout-minutes: 5 + - name: Install k3d and create registry + run: | + wget -qO - https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | TAG=$K3D_VERSION bash + k3d registry create oci.localhost --port 5001 + - name: Run tests + run: | + make -C tests/e2e test-create-cmd + timeout-minutes: 3 diff --git a/.github/workflows/e2e-test-create-scaffold.yml b/.github/workflows/test-e2e-scaffold.yml similarity index 62% rename from .github/workflows/e2e-test-create-scaffold.yml rename to .github/workflows/test-e2e-scaffold.yml index ba03bfba..cb80b104 100644 --- a/.github/workflows/e2e-test-create-scaffold.yml +++ b/.github/workflows/test-e2e-scaffold.yml @@ -1,29 +1,28 @@ -name: E2E Test - Create Scaffold +name: E2E test - scaffold command on: pull_request: branches: - main - 'release-**' jobs: - e2e-test: - name: E2E Test - Create Scaffold + test-scaffold-cmd: runs-on: ubuntu-latest steps: - - name: Checkout modulectl + - name: Checkout uses: actions/checkout@v4 - - name: Set up Go + - name: Go setup uses: actions/setup-go@v4 with: go-version-file: 'go.mod' cache-dependency-path: 'go.sum' - - name: Build modulectl + - name: Build run: | make build-linux chmod +x ./bin/modulectl-linux ls -la ./bin mv ./bin/modulectl-linux /usr/local/bin/modulectl - timeout-minutes: 10 - - name: Run E2E Test - Create Scaffold + timeout-minutes: 5 + - name: Run tests run: | - make -C tests/e2e test-create-scaffold + make -C tests/e2e test-scaffold-cmd timeout-minutes: 3 diff --git a/.github/workflows/verify-unit-test-coverage.yml b/.github/workflows/test-unit-coverage.yml similarity index 100% rename from .github/workflows/verify-unit-test-coverage.yml rename to .github/workflows/test-unit-coverage.yml diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/test-unit.yaml similarity index 100% rename from .github/workflows/unit-test.yaml rename to .github/workflows/test-unit.yaml diff --git a/.gitignore b/.gitignore index deab2fdc..8fa66441 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,6 @@ bin # rendered manifest **/manifest.yaml + +# copied template-operator repo +tests/e2e/create/testdata/template-operator diff --git a/.golangci.yaml b/.golangci.yaml index 385d97d3..8e41fd94 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -6,7 +6,7 @@ linters: - exhaustruct # too subjective and harms code readability - execinquery # deprecated (since v1.58.0) - exportloopref # deprecated (since v1.60.2), replaced by copyloopvar - - forbidigo # temparily disabled + - forbidigo # temporarily disabled - godot # not needed - gomnd # deprecated (since v1.58.0), renamed to mnd - lll @@ -60,6 +60,12 @@ linters-settings: alias: scaffoldcmd - pkg: github.com/kyma-project/modulectl/cmd/modulectl/create alias: createcmd + - pkg: github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1 + alias: ocmmetav1 + - pkg: github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/versions/v2 + alias: compdescv2 + - pkg: github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/ocireg + alias: ocmocireg - pkg: github.com/kyma-project/modulectl/internal/service/moduleconfig/generator alias: moduleconfiggenerator - pkg: github.com/kyma-project/modulectl/internal/service/moduleconfig/reader @@ -87,7 +93,6 @@ linters-settings: ignore-type-assert-ok: true ignore-map-index-ok: true ignore-chan-recv-ok: true - godox: # `TODO`, `BUG`, `FIXME` are the default, no need to mention again issues: exclude-rules: - path: "_test\\.go" diff --git a/cmd/modulectl/create/cmd.go b/cmd/modulectl/create/cmd.go index 29bdb7ed..549b166a 100644 --- a/cmd/modulectl/create/cmd.go +++ b/cmd/modulectl/create/cmd.go @@ -24,13 +24,13 @@ var long string //go:embed example.txt var example string -type ModuleService interface { - CreateModule(opts create.Options) error +type Service interface { + Run(opts create.Options) error } -func NewCmd(moduleService ModuleService) (*cobra.Command, error) { - if moduleService == nil { - return nil, fmt.Errorf("%w: createService must not be nil", commonerrors.ErrInvalidArg) +func NewCmd(service Service) (*cobra.Command, error) { + if service == nil { + return nil, fmt.Errorf("%w: service must not be nil", commonerrors.ErrInvalidArg) } opts := create.Options{} @@ -41,7 +41,7 @@ func NewCmd(moduleService ModuleService) (*cobra.Command, error) { Long: long, Example: example, RunE: func(cmd *cobra.Command, _ []string) error { - return moduleService.CreateModule(opts) + return service.Run(opts) }, } diff --git a/cmd/modulectl/create/cmd_test.go b/cmd/modulectl/create/cmd_test.go index 073ce58d..5e8c6a82 100644 --- a/cmd/modulectl/create/cmd_test.go +++ b/cmd/modulectl/create/cmd_test.go @@ -18,7 +18,7 @@ func Test_NewCmd_ReturnsError_WhenModuleServiceIsNil(t *testing.T) { _, err := createcmd.NewCmd(nil) require.Error(t, err) - assert.Contains(t, err.Error(), "createService") + assert.Contains(t, err.Error(), "service must not be nil") } func Test_NewCmd_Succeeds(t *testing.T) { @@ -130,7 +130,7 @@ type moduleServiceStub struct { opts create.Options } -func (m *moduleServiceStub) CreateModule(opts create.Options) error { +func (m *moduleServiceStub) Run(opts create.Options) error { m.called = true m.opts = opts return nil @@ -140,6 +140,6 @@ type moduleServiceErrorStub struct{} var errSomeTestError = errors.New("some test error") -func (s *moduleServiceErrorStub) CreateModule(_ create.Options) error { +func (s *moduleServiceErrorStub) Run(_ create.Options) error { return errSomeTestError } diff --git a/cmd/modulectl/scaffold/cmd.go b/cmd/modulectl/scaffold/cmd.go index f0e9c662..2ec3c380 100644 --- a/cmd/modulectl/scaffold/cmd.go +++ b/cmd/modulectl/scaffold/cmd.go @@ -24,13 +24,13 @@ var long string //go:embed example.txt var example string -type ScaffoldService interface { - CreateScaffold(opts scaffold.Options) error +type Service interface { + Run(opts scaffold.Options) error } -func NewCmd(scaffoldService ScaffoldService) (*cobra.Command, error) { - if scaffoldService == nil { - return nil, fmt.Errorf("%w: scaffoldService must not be nil", commonerrors.ErrInvalidArg) +func NewCmd(service Service) (*cobra.Command, error) { + if service == nil { + return nil, fmt.Errorf("%w: service must not be nil", commonerrors.ErrInvalidArg) } opts := scaffold.Options{} @@ -42,7 +42,7 @@ func NewCmd(scaffoldService ScaffoldService) (*cobra.Command, error) { Example: example, Args: cobra.NoArgs, RunE: func(_ *cobra.Command, _ []string) error { - return scaffoldService.CreateScaffold(opts) + return service.Run(opts) }, } diff --git a/cmd/modulectl/scaffold/cmd_test.go b/cmd/modulectl/scaffold/cmd_test.go index f7826c86..a97d78d8 100644 --- a/cmd/modulectl/scaffold/cmd_test.go +++ b/cmd/modulectl/scaffold/cmd_test.go @@ -17,7 +17,7 @@ func Test_NewCmd_ReturnsError_WhenScaffoldServiceIsNil(t *testing.T) { _, err := scaffoldcmd.NewCmd(nil) require.Error(t, err) - assert.Contains(t, err.Error(), "scaffoldService") + assert.Contains(t, err.Error(), "service must not be nil") } func Test_NewCmd_Succceeds(t *testing.T) { @@ -143,7 +143,7 @@ type scaffoldServiceStub struct { opts scaffold.Options } -func (s *scaffoldServiceStub) CreateScaffold(opts scaffold.Options) error { +func (s *scaffoldServiceStub) Run(opts scaffold.Options) error { s.called = true s.opts = opts return nil @@ -153,6 +153,6 @@ type scaffoldServiceErrorStub struct{} var errSomeTestError = errors.New("some test error") -func (s *scaffoldServiceErrorStub) CreateScaffold(_ scaffold.Options) error { +func (s *scaffoldServiceErrorStub) Run(_ scaffold.Options) error { return errSomeTestError } diff --git a/go.mod b/go.mod index b7cdef5a..89a34448 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/mandelsoft/vfs v0.4.3 github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.34.2 + github.com/open-component-model/ocm v0.13.0 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 @@ -120,6 +121,7 @@ require ( github.com/elliotchance/orderedmap v1.6.0 // indirect github.com/emicklei/go-restful/v3 v3.11.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -311,6 +313,7 @@ require ( golang.org/x/time v0.6.0 // indirect golang.org/x/tools v0.24.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/api v0.190.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect @@ -330,6 +333,7 @@ require ( k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect oras.land/oras-go v1.2.5 // indirect + sigs.k8s.io/controller-runtime v0.19.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.17.3 // indirect sigs.k8s.io/kustomize/kyaml v0.17.2 // indirect diff --git a/go.sum b/go.sum index 1ed54c40..4f741a3c 100644 --- a/go.sum +++ b/go.sum @@ -368,6 +368,7 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= +github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= @@ -427,6 +428,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= @@ -783,6 +786,8 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/open-component-model/ocm v0.13.0 h1:rm31Z7SpFzpxCIUagaFEUI4cSIS098Hf2H0dToV2nOA= +github.com/open-component-model/ocm v0.13.0/go.mod h1:+ovmIxTexDM2fcnfVg9uqJh22A7KOxwa/stNJn315yU= github.com/open-policy-agent/opa v0.67.0 h1:FOdsO9yNhfmrh+72oVK7ImWmzruG+VSpfbr5IBqEWVs= github.com/open-policy-agent/opa v0.67.0/go.mod h1:aqKlHc8E2VAAylYE9x09zJYr/fYzGX+JKne89UGqFzk= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -1230,6 +1235,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/api v0.190.0 h1:ASM+IhLY1zljNdLu19W1jTmU6A+gMk6M46Wlur61s+Q= google.golang.org/api v0.190.0/go.mod h1:QIr6I9iedBLnfqoD6L6Vze1UvS5Hzj5r2aUBOaZnLHo= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= diff --git a/internal/service/create/create.go b/internal/service/create/create.go index 7402bc85..9ea70a48 100644 --- a/internal/service/create/create.go +++ b/internal/service/create/create.go @@ -111,7 +111,7 @@ func NewService(moduleConfigService ModuleConfigService, }, nil } -func (s *Service) CreateModule(opts Options) error { +func (s *Service) Run(opts Options) error { if err := opts.Validate(); err != nil { return err } diff --git a/internal/service/create/create_test.go b/internal/service/create/create_test.go index 5b41bbe2..40474d2a 100644 --- a/internal/service/create/create_test.go +++ b/internal/service/create/create_test.go @@ -33,7 +33,7 @@ func Test_CreateModule_ReturnsError_WhenModuleConfigFileIsEmpty(t *testing.T) { opts := newCreateOptionsBuilder().withModuleConfigFile("").build() - err = svc.CreateModule(opts) + err = svc.Run(opts) require.ErrorIs(t, err, commonerrors.ErrInvalidOption) require.Contains(t, err.Error(), "opts.ModuleConfigFile") @@ -46,7 +46,7 @@ func Test_CreateModule_ReturnsError_WhenOutIsNil(t *testing.T) { opts := newCreateOptionsBuilder().withOut(nil).build() - err = svc.CreateModule(opts) + err = svc.Run(opts) require.ErrorIs(t, err, commonerrors.ErrInvalidOption) require.Contains(t, err.Error(), "opts.Out") @@ -59,7 +59,7 @@ func Test_CreateModule_ReturnsError_WhenCredentialsIsInInvalidFormat(t *testing. opts := newCreateOptionsBuilder().withCredentials("user").build() - err = svc.CreateModule(opts) + err = svc.Run(opts) require.ErrorIs(t, err, commonerrors.ErrInvalidOption) require.Contains(t, err.Error(), "opts.Credentials") @@ -72,7 +72,7 @@ func Test_CreateModule_ReturnsError_WhenTemplateOutputIsEmpty(t *testing.T) { opts := newCreateOptionsBuilder().withTemplateOutput("").build() - err = svc.CreateModule(opts) + err = svc.Run(opts) require.ErrorIs(t, err, commonerrors.ErrInvalidOption) require.Contains(t, err.Error(), "opts.TemplateOutput") @@ -86,7 +86,7 @@ func Test_CreateModule_ReturnsError_WhenParseAndValidateModuleConfigReturnsError opts := newCreateOptionsBuilder().build() - err = svc.CreateModule(opts) + err = svc.Run(opts) require.Error(t, err) require.Contains(t, err.Error(), "failed to read module config file") diff --git a/internal/service/scaffold/options.go b/internal/service/scaffold/options.go index 71c079e8..8929c3e1 100644 --- a/internal/service/scaffold/options.go +++ b/internal/service/scaffold/options.go @@ -23,7 +23,7 @@ type Options struct { ModuleChannel string } -func (opts Options) validate() error { +func (opts Options) Validate() error { if opts.Out == nil { return fmt.Errorf("%w: opts.Out must not be nil", commonerrors.ErrInvalidOption) } diff --git a/internal/service/scaffold/options_test.go b/internal/service/scaffold/options_test.go new file mode 100644 index 00000000..de0d4e5c --- /dev/null +++ b/internal/service/scaffold/options_test.go @@ -0,0 +1,188 @@ +package scaffold_test + +import ( + "io" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/kyma-project/modulectl/internal/service/scaffold" + iotools "github.com/kyma-project/modulectl/tools/io" +) + +func Test_Validate_Options(t *testing.T) { + tests := []struct { + name string + options scaffold.Options + wantErr bool + errMsg string + }{ + { + name: "Out is nil", + options: scaffold.Options{Out: nil}, + wantErr: true, + errMsg: "opts.Out must not be nil", + }, + { + name: "ModuleName is empty", + options: scaffold.Options{ + Out: iotools.NewDefaultOut(io.Discard), + ModuleName: "", + }, + wantErr: true, + errMsg: "opts.ModuleName must not be empty", + }, + { + name: "ModuleName exceeds length", + options: scaffold.Options{ + Out: iotools.NewDefaultOut(io.Discard), + ModuleName: strings.Repeat("a", 256), + }, + wantErr: true, + errMsg: "opts.ModuleName length must not exceed", + }, + { + name: "ModuleName invalid pattern", + options: scaffold.Options{ + Out: iotools.NewDefaultOut(io.Discard), + ModuleName: "invalid_name", + }, + wantErr: true, + errMsg: "opts.ModuleName must match the required pattern", + }, + { + name: "Directory is empty", + options: scaffold.Options{ + Out: iotools.NewDefaultOut(io.Discard), + ModuleName: "github.com/kyma-project/test", + Directory: "", + }, + wantErr: true, + errMsg: "opts.Directory must not be empty", + }, + { + name: "ModuleVersion is empty", + options: scaffold.Options{ + Out: iotools.NewDefaultOut(io.Discard), + ModuleName: "github.com/kyma-project/test", + Directory: "./", + ModuleVersion: "", + }, + wantErr: true, + errMsg: "opts.ModuleVersion must not be empty", + }, + { + name: "ModuleVersion invalid", + options: scaffold.Options{ + Out: iotools.NewDefaultOut(io.Discard), + ModuleName: "github.com/kyma-project/test", + Directory: "./", + ModuleVersion: "invalid", + }, + wantErr: true, + errMsg: "opts.ModuleVersion failed to parse as semantic version", + }, + { + name: "ModuleChannel is empty", + options: scaffold.Options{ + Out: iotools.NewDefaultOut(io.Discard), + ModuleName: "github.com/kyma-project/test", + Directory: "./", + ModuleVersion: "0.0.1", + ModuleChannel: "", + }, + wantErr: true, + errMsg: "opts.ModuleChannel must not be empty", + }, + { + name: "ModuleChannel exceeds length", + options: scaffold.Options{ + Out: iotools.NewDefaultOut(io.Discard), + ModuleName: "github.com/kyma-project/test", + Directory: "./", + ModuleVersion: "0.0.1", + ModuleChannel: strings.Repeat("a", 33), + }, + wantErr: true, + errMsg: "opts.ModuleChannel length must not exceed", + }, + { + name: "ModuleChannel below minimum length", + options: scaffold.Options{ + Out: iotools.NewDefaultOut(io.Discard), + ModuleName: "github.com/kyma-project/test", + Directory: "./", + ModuleVersion: "0.0.1", + ModuleChannel: "aa", + }, + wantErr: true, + errMsg: "opts.ModuleChannel length must be at least", + }, + { + name: "ModuleChannel invalid pattern", + options: scaffold.Options{ + Out: iotools.NewDefaultOut(io.Discard), + ModuleName: "github.com/kyma-project/test", + Directory: "./", + ModuleVersion: "0.0.1", + ModuleChannel: "invalid_channel", + }, + wantErr: true, + errMsg: "opts.ModuleChannel must match the required pattern", + }, + { + name: "ModuleConfigFileName is empty", + options: scaffold.Options{ + Out: iotools.NewDefaultOut(io.Discard), + ModuleName: "github.com/kyma-project/test", + Directory: "./", + ModuleVersion: "0.0.1", + ModuleChannel: "stable", + ModuleConfigFileName: "", + }, + wantErr: true, + errMsg: "opts.ModuleConfigFileName must not be empty", + }, + { + name: "ManifestFileName is empty", + options: scaffold.Options{ + Out: iotools.NewDefaultOut(io.Discard), + ModuleName: "github.com/kyma-project/test", + Directory: "./", + ModuleVersion: "0.0.1", + ModuleChannel: "stable", + ModuleConfigFileName: "config.yaml", + ManifestFileName: "", + }, + wantErr: true, + errMsg: "opts.ManifestFileName must not be empty", + }, + { + name: "All fields valid", + options: scaffold.Options{ + Out: iotools.NewDefaultOut(io.Discard), + ModuleName: "github.com/kyma-project/test", + Directory: "./", + ModuleVersion: "0.0.1", + ModuleChannel: "stable", + ModuleConfigFileName: "config.yaml", + ManifestFileName: "manifest.yaml", + SecurityConfigFileName: "", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.options.Validate() + if tt.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errMsg) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/internal/service/scaffold/scaffold.go b/internal/service/scaffold/scaffold.go index cc60d81a..95efc2a1 100644 --- a/internal/service/scaffold/scaffold.go +++ b/internal/service/scaffold/scaffold.go @@ -55,8 +55,8 @@ func NewService(moduleConfigService ModuleConfigService, }, nil } -func (s *Service) CreateScaffold(opts Options) error { - if err := opts.validate(); err != nil { +func (s *Service) Run(opts Options) error { + if err := opts.Validate(); err != nil { return err } diff --git a/internal/service/scaffold/scaffold_test.go b/internal/service/scaffold/scaffold_test.go index bc02cc90..8355b2f3 100644 --- a/internal/service/scaffold/scaffold_test.go +++ b/internal/service/scaffold/scaffold_test.go @@ -11,7 +11,6 @@ import ( commonerrors "github.com/kyma-project/modulectl/internal/common/errors" "github.com/kyma-project/modulectl/internal/common/types" "github.com/kyma-project/modulectl/internal/service/scaffold" - "github.com/kyma-project/modulectl/internal/testutils" iotools "github.com/kyma-project/modulectl/tools/io" ) @@ -59,194 +58,6 @@ func Test_NewService_ReturnsError_WhenSecurityConfigServiceIsNil(t *testing.T) { assert.Contains(t, err.Error(), "securityConfigService") } -func Test_CreateScaffold_ReturnsError_WhenOutIsNil(t *testing.T) { - svc, _ := scaffold.NewService( - &moduleConfigForceExplicitOverwriteErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}) - opts := newScaffoldOptionsBuilder().withOut(nil).build() - - result := svc.CreateScaffold(opts) - - require.ErrorIs(t, result, commonerrors.ErrInvalidOption) - assert.Contains(t, result.Error(), "opts.Out") -} - -func Test_CreateScaffold_ReturnsError_WhenDirectoryIsEmpty(t *testing.T) { - svc, _ := scaffold.NewService( - &moduleConfigForceExplicitOverwriteErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}) - opts := newScaffoldOptionsBuilder().withDirectory("").build() - - result := svc.CreateScaffold(opts) - - require.ErrorIs(t, result, commonerrors.ErrInvalidOption) - assert.Contains(t, result.Error(), "opts.Directory") -} - -func Test_CreateScaffold_ReturnsError_WhenModuleConfigFileIsEmpty(t *testing.T) { - svc, _ := scaffold.NewService( - &moduleConfigForceExplicitOverwriteErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}) - opts := newScaffoldOptionsBuilder().withModuleConfigFileName("").build() - - result := svc.CreateScaffold(opts) - - require.ErrorIs(t, result, commonerrors.ErrInvalidOption) - assert.Contains(t, result.Error(), "opts.ModuleConfigFileName") -} - -func Test_CreateScaffold_ReturnsError_WhenManifestFileIsEmpty(t *testing.T) { - svc, _ := scaffold.NewService( - &moduleConfigForceExplicitOverwriteErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}) - opts := newScaffoldOptionsBuilder().withManifestFileName("").build() - - result := svc.CreateScaffold(opts) - - require.ErrorIs(t, result, commonerrors.ErrInvalidOption) - assert.Contains(t, result.Error(), "opts.ManifestFileName") -} - -func Test_CreateScaffold_ReturnsError_WhenModuleNameIsEmpty(t *testing.T) { - svc, _ := scaffold.NewService( - &moduleConfigForceExplicitOverwriteErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}) - opts := newScaffoldOptionsBuilder().withModuleName("").build() - - result := svc.CreateScaffold(opts) - - require.ErrorIs(t, result, commonerrors.ErrInvalidOption) - assert.Contains(t, result.Error(), "opts.ModuleName") -} - -func Test_CreateScaffold_ReturnsError_WhenModuleNameIsExceedingLength(t *testing.T) { - svc, _ := scaffold.NewService( - &moduleConfigForceExplicitOverwriteErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}) - opts := newScaffoldOptionsBuilder().withModuleName(testutils.RandomName(256)).build() - - result := svc.CreateScaffold(opts) - - require.ErrorIs(t, result, commonerrors.ErrInvalidOption) - assert.Contains(t, result.Error(), "opts.ModuleName") - assert.Contains(t, result.Error(), "length") -} - -func Test_CreateScaffold_ReturnsError_WhenModuleNameIsNotMatchingPattern(t *testing.T) { - svc, _ := scaffold.NewService( - &moduleConfigForceExplicitOverwriteErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}) - opts := newScaffoldOptionsBuilder().withModuleName(testutils.RandomName(10)).build() - - result := svc.CreateScaffold(opts) - - require.ErrorIs(t, result, commonerrors.ErrInvalidOption) - assert.Contains(t, result.Error(), "opts.ModuleName") - assert.Contains(t, result.Error(), "pattern") -} - -func Test_CreateScaffold_ReturnsError_WhenModuleVersionIsEmpty(t *testing.T) { - svc, _ := scaffold.NewService( - &moduleConfigForceExplicitOverwriteErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}) - opts := newScaffoldOptionsBuilder().withModuleVersion("").build() - - result := svc.CreateScaffold(opts) - - require.ErrorIs(t, result, commonerrors.ErrInvalidOption) - assert.Contains(t, result.Error(), "opts.ModuleVersion") -} - -func Test_CreateScaffold_ReturnsError_WhenModuleVersionIsInvalid(t *testing.T) { - svc, _ := scaffold.NewService( - &moduleConfigForceExplicitOverwriteErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}) - opts := newScaffoldOptionsBuilder().withModuleVersion(testutils.RandomName(10)).build() - - result := svc.CreateScaffold(opts) - - require.ErrorIs(t, result, commonerrors.ErrInvalidOption) - assert.Contains(t, result.Error(), "opts.ModuleVersion") - assert.Contains(t, result.Error(), "failed to parse") -} - -func Test_CreateScaffold_ReturnsError_WhenModuleChannelIsEmpty(t *testing.T) { - svc, _ := scaffold.NewService( - &moduleConfigForceExplicitOverwriteErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}) - opts := newScaffoldOptionsBuilder().withModuleChannel("").build() - - result := svc.CreateScaffold(opts) - - require.ErrorIs(t, result, commonerrors.ErrInvalidOption) - assert.Contains(t, result.Error(), "opts.ModuleChannel") -} - -func Test_CreateScaffold_ReturnsError_WhenModuleChannelIsExceedingLength(t *testing.T) { - svc, _ := scaffold.NewService( - &moduleConfigForceExplicitOverwriteErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}) - opts := newScaffoldOptionsBuilder().withModuleChannel(testutils.RandomName(33)).build() - - result := svc.CreateScaffold(opts) - - require.ErrorIs(t, result, commonerrors.ErrInvalidOption) - assert.Contains(t, result.Error(), "opts.ModuleChannel") - assert.Contains(t, result.Error(), "length") -} - -func Test_CreateScaffold_ReturnsError_WhenModuleChannelFallsBelowLength(t *testing.T) { - svc, _ := scaffold.NewService( - &moduleConfigForceExplicitOverwriteErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}) - opts := newScaffoldOptionsBuilder().withModuleChannel(testutils.RandomName(2)).build() - - result := svc.CreateScaffold(opts) - - require.ErrorIs(t, result, commonerrors.ErrInvalidOption) - assert.Contains(t, result.Error(), "opts.ModuleChannel") - assert.Contains(t, result.Error(), "length") -} - -func Test_CreateScaffold_ReturnsError_WhenModuleChannelNotMatchingCharset(t *testing.T) { - svc, _ := scaffold.NewService( - &moduleConfigForceExplicitOverwriteErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}, - &fileGeneratorErrorStub{}) - opts := newScaffoldOptionsBuilder().withModuleChannel("with not allowed chars 123%&").build() - - result := svc.CreateScaffold(opts) - - require.ErrorIs(t, result, commonerrors.ErrInvalidOption) - assert.Contains(t, result.Error(), "opts.ModuleChannel") - assert.Contains(t, result.Error(), "pattern") -} - func Test_CreateScaffold_ReturnsError_WhenModuleConfigServiceForceExplicitOverwriteReturnsError(t *testing.T) { svc, _ := scaffold.NewService( &moduleConfigForceExplicitOverwriteErrorStub{}, @@ -254,7 +65,7 @@ func Test_CreateScaffold_ReturnsError_WhenModuleConfigServiceForceExplicitOverwr &fileGeneratorErrorStub{}, &fileGeneratorErrorStub{}) - result := svc.CreateScaffold(newScaffoldOptionsBuilder().build()) + result := svc.Run(newScaffoldOptionsBuilder().build()) require.ErrorIs(t, result, errOverwriteError) } @@ -266,7 +77,7 @@ func Test_CreateScaffold_ReturnsError_WhenGeneratingManifestFileFails(t *testing &fileGeneratorErrorStub{}, &fileGeneratorErrorStub{}) - result := svc.CreateScaffold(newScaffoldOptionsBuilder().build()) + result := svc.Run(newScaffoldOptionsBuilder().build()) require.ErrorIs(t, result, scaffold.ErrGeneratingFile) require.ErrorIs(t, result, errSomeFileGeneratorError) @@ -280,7 +91,7 @@ func Test_CreateScaffold_Succeeds_WhenGeneratingManifestFile(t *testing.T) { &fileGeneratorStub{}, &fileGeneratorStub{}) - result := svc.CreateScaffold(newScaffoldOptionsBuilder().build()) + result := svc.Run(newScaffoldOptionsBuilder().build()) require.NoError(t, result) } @@ -292,7 +103,7 @@ func Test_CreateScaffold_Succeeds_WhenDefaultCRFileIsNotConfigured(t *testing.T) &fileGeneratorErrorStub{}, &fileGeneratorStub{}) - result := svc.CreateScaffold(newScaffoldOptionsBuilder().withDefaultCRFileName("").build()) + result := svc.Run(newScaffoldOptionsBuilder().withDefaultCRFileName("").build()) require.NoError(t, result) } @@ -304,7 +115,7 @@ func Test_CreateScaffold_ReturnsError_WhenGeneratingDefaultCRFileFails(t *testin &fileGeneratorErrorStub{}, &fileGeneratorErrorStub{}) - result := svc.CreateScaffold(newScaffoldOptionsBuilder().build()) + result := svc.Run(newScaffoldOptionsBuilder().build()) require.ErrorIs(t, result, scaffold.ErrGeneratingFile) require.ErrorIs(t, result, errSomeFileGeneratorError) @@ -318,19 +129,7 @@ func Test_CreateScaffold_Succeeds_WhenGeneratingDefaultCRFile(t *testing.T) { &fileGeneratorStub{}, &fileGeneratorStub{}) - result := svc.CreateScaffold(newScaffoldOptionsBuilder().build()) - - require.NoError(t, result) -} - -func Test_CreateScaffold_Succeeds_WhenSecurityConfigFileIsNotConfigured(t *testing.T) { - svc, _ := scaffold.NewService( - &moduleConfigStub{}, - &fileGeneratorStub{}, - &fileGeneratorStub{}, - &fileGeneratorErrorStub{}) - - result := svc.CreateScaffold(newScaffoldOptionsBuilder().withSecurityConfigFileName("").build()) + result := svc.Run(newScaffoldOptionsBuilder().build()) require.NoError(t, result) } @@ -342,7 +141,7 @@ func Test_CreateScaffold_ReturnsError_WhenGeneratingSecurityConfigFileFails(t *t &fileGeneratorStub{}, &fileGeneratorErrorStub{}) - result := svc.CreateScaffold(newScaffoldOptionsBuilder().build()) + result := svc.Run(newScaffoldOptionsBuilder().build()) require.ErrorIs(t, result, scaffold.ErrGeneratingFile) require.ErrorIs(t, result, errSomeFileGeneratorError) @@ -356,7 +155,7 @@ func Test_CreateScaffold_Succeeds_WhenGeneratingSecurityConfigFile(t *testing.T) &fileGeneratorStub{}, &fileGeneratorStub{}) - result := svc.CreateScaffold(newScaffoldOptionsBuilder().build()) + result := svc.Run(newScaffoldOptionsBuilder().build()) require.NoError(t, result) } @@ -368,7 +167,7 @@ func Test_CreateScaffold_ReturnsError_WhenGeneratingModuleConfigReturnsError(t * &fileGeneratorStub{}, &fileGeneratorStub{}) - result := svc.CreateScaffold(newScaffoldOptionsBuilder().build()) + result := svc.Run(newScaffoldOptionsBuilder().build()) require.ErrorIs(t, result, scaffold.ErrGeneratingFile) require.ErrorIs(t, result, errSomeFileGeneratorError) @@ -382,7 +181,7 @@ func Test_CreateScaffold_Succeeds(t *testing.T) { &fileGeneratorStub{}, &fileGeneratorStub{}) - result := svc.CreateScaffold(newScaffoldOptionsBuilder().build()) + result := svc.Run(newScaffoldOptionsBuilder().build()) require.NoError(t, result) } diff --git a/tests/e2e/Makefile b/tests/e2e/Makefile index b64990e4..2e62884a 100644 --- a/tests/e2e/Makefile +++ b/tests/e2e/Makefile @@ -13,5 +13,8 @@ SHELL = /usr/bin/env bash -o pipefail ##@ E2E Tests -test-create-scaffold: - go test ./scaffold -ginkgo.v -ginkgo.focus "Create Scaffold Command" +test-scaffold-cmd: + go test -tags=e2e ./scaffold -ginkgo.v -ginkgo.focus "Test 'scaffold' command" + +test-create-cmd: + go test -tags=e2e ./create -ginkgo.v -ginkgo.focus "Test 'create' command" diff --git a/tests/e2e/create/create_suite_test.go b/tests/e2e/create/create_suite_test.go new file mode 100644 index 00000000..48a67953 --- /dev/null +++ b/tests/e2e/create/create_suite_test.go @@ -0,0 +1,84 @@ +//go:build e2e + +package create_test + +import ( + "fmt" + "os/exec" + "strings" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func Test_Create(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "'Create' Command Test Suite") +} + +const ( + testdataDir = "./testdata/moduleconfig/" + + invalidConfigs = testdataDir + "invalid/" + missingNameConfig = invalidConfigs + "missing-name.yaml" + missingChannelConfig = invalidConfigs + "missing-channel.yaml" + missingVersionConfig = invalidConfigs + "missing-version.yaml" + missingManifestConfig = invalidConfigs + "missing-manifest.yaml" + + validConfigs = testdataDir + "valid/" + minimalConfig = validConfigs + "minimal.yaml" + withAnnotationsConfig = validConfigs + "with-annotations.yaml" + withDefaultCrConfig = validConfigs + "with-defaultcr.yaml" + withSecurityConfig = validConfigs + "with-security.yaml" + withMandatoryConfig = validConfigs + "with-mandatory.yaml" + + ociRegistry = "http://k3d-oci.localhost:5001" + templateOutputPath = "/tmp/template.yaml" + gitRemote = "https://github.com/kyma-project/template-operator" +) + +// Command wrapper for `modulectl create` + +type createCmd struct { + registry string + output string + moduleConfigFile string + gitRemote string + insecure bool +} + +func (cmd *createCmd) execute() error { + var command *exec.Cmd + + args := []string{"create"} + + if cmd.moduleConfigFile != "" { + args = append(args, "--module-config-file="+cmd.moduleConfigFile) + } + + if cmd.registry != "" { + args = append(args, "--registry="+cmd.registry) + } + + if cmd.output != "" { + args = append(args, "--output="+cmd.output) + } + + if cmd.gitRemote != "" { + args = append(args, "--git-remote="+cmd.gitRemote) + } + + if cmd.insecure { + args = append(args, "--insecure") + } + + println(" >>> Executing command: modulectl", strings.Join(args, " ")) + + command = exec.Command("modulectl", args...) + cmdOut, err := command.CombinedOutput() + if err != nil { + return fmt.Errorf("create command failed with output: %s and error: %w", cmdOut, err) + } + return nil +} diff --git a/tests/e2e/create/create_test.go b/tests/e2e/create/create_test.go new file mode 100644 index 00000000..9423c189 --- /dev/null +++ b/tests/e2e/create/create_test.go @@ -0,0 +1,456 @@ +//go:build e2e + +package create_test + +import ( + "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/github" + "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/ociartifact" + "io/fs" + "os" + + "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/ocireg" + "github.com/open-component-model/ocm/pkg/contexts/ocm" + "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/localblob" + "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc" + ocmmetav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" + compdescv2 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/versions/v2" + ocmocireg "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/ocireg" + "k8s.io/apimachinery/pkg/util/yaml" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Test 'create' command", Ordered, func() { + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked without any args", func() { + cmd = createCmd{} + }) + + It("Then the command should fail", func() { + err := cmd.execute() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("failed to read file module-config.yaml: open module-config.yaml: no such file or directory")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with '--module-config-file' using file with missing name", func() { + cmd = createCmd{ + moduleConfigFile: missingNameConfig, + } + }) + It("Then the command should fail", func() { + err := cmd.execute() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("invalid Option: opts.ModuleName must not be empty")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with '--module-config-file' using file with missing channel", func() { + cmd = createCmd{ + moduleConfigFile: missingChannelConfig, + } + }) + It("Then the command should fail", func() { + err := cmd.execute() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("invalid Option: opts.ModuleChannel must not be empty")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with '--module-config-file' using file with missing version", func() { + cmd = createCmd{ + moduleConfigFile: missingVersionConfig, + } + }) + It("Then the command should fail", func() { + err := cmd.execute() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("invalid Option: opts.ModuleVersion must not be empty")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with '--module-config-file' using file with missing manifest", func() { + cmd = createCmd{ + moduleConfigFile: missingManifestConfig, + } + }) + It("Then the command should fail", func() { + err := cmd.execute() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("failed to parse module config: failed to value module config: manifest path must not be empty: invalid Option")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with '--module-config-file' using valid file", func() { + cmd = createCmd{ + moduleConfigFile: minimalConfig, + } + }) + It("Then the command should succeed", func() { + Expect(cmd.execute()).To(Succeed()) + + By("And no module template file should be generated") + currentDir, err := os.Getwd() + Expect(err).ToNot(HaveOccurred()) + Expect(filesIn(currentDir)).Should(Not(ContainElement("template.yaml"))) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with existing '--registry' and missing '--insecure' flag", func() { + cmd = createCmd{ + moduleConfigFile: minimalConfig, + registry: ociRegistry, + } + }) + It("Then the command should fail", func() { + err := cmd.execute() + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("Error: could not push")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with minimal valid module-config", func() { + cmd = createCmd{ + moduleConfigFile: minimalConfig, + 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(descriptor.SchemaVersion()).To(Equal(compdescv2.SchemaVersion)) + + By("And annotations should be correct") + annotations := template.Annotations + Expect(annotations[shared.ModuleVersionAnnotation]).To(Equal("1.0.0")) + Expect(annotations[shared.IsClusterScopedAnnotation]).To(Equal("false")) + + By("And descriptor.component.repositoryContexts should be correct") + Expect(descriptor.RepositoryContexts).To(HaveLen(1)) + repo := descriptor.GetEffectiveRepositoryContext() + Expect(repo.Object["baseUrl"]).To(Equal(ociRegistry)) + Expect(repo.Object["componentNameMapping"]).To(Equal(string(ocmocireg.OCIRegistryURLPathMapping))) + Expect(repo.Object["type"]).To(Equal(ocireg.Type)) + + By("And descriptor.component.resources should be correct") + Expect(descriptor.Resources).To(HaveLen(1)) + resource := descriptor.Resources[0] + Expect(resource.Name).To(Equal("raw-manifest")) + Expect(resource.Relation).To(Equal(ocmmetav1.LocalRelation)) + Expect(resource.Type).To(Equal("directory")) + Expect(resource.Version).To(Equal("1.0.0")) + + By("And descriptor.component.resources[0].access should be correct") + resourceAccessSpec1, err := ocm.DefaultContext().AccessSpecForSpec(descriptor.Resources[0].Access) + Expect(err).ToNot(HaveOccurred()) + localBlobAccessSpec, ok := resourceAccessSpec1.(*localblob.AccessSpec) + Expect(ok).To(BeTrue()) + Expect(localBlobAccessSpec.GetType()).To(Equal(localblob.Type)) + Expect(localBlobAccessSpec.LocalReference).To(ContainSubstring("sha256:")) + Expect(localBlobAccessSpec.MediaType).To(Equal("application/x-tar")) + + By("And descriptor.component.sources should be empty") + Expect(len(descriptor.Sources)).To(Equal(0)) + + By("And spec.mandatory should be false") + Expect(template.Spec.Mandatory).To(BeFalse()) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with same version that already exists in the registry", func() { + cmd = createCmd{ + moduleConfigFile: minimalConfig, + registry: ociRegistry, + insecure: true, + } + }) + It("Then the command should fail with same version exists message", func() { + err := cmd.execute() + Expect(err.Error()).Should(ContainSubstring("could not push component version: component version already exists")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with valid module-config containing annotations and different version", func() { + cmd = createCmd{ + moduleConfigFile: withAnnotationsConfig, + 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 new annotation should be correctly added") + annotations := template.Annotations + Expect(annotations[shared.ModuleVersionAnnotation]).To(Equal("1.0.1")) + Expect(annotations[shared.IsClusterScopedAnnotation]).To(Equal("false")) + Expect(annotations["operator.kyma-project.io/doc-url"]).To(Equal("https://kyma-project.io")) + + By("And descriptor.component.resources should be correct") + resource := descriptor.Resources[0] + Expect(resource.Version).To(Equal("1.0.1")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with valid module-config containing default-cr and different version", func() { + cmd = createCmd{ + moduleConfigFile: withDefaultCrConfig, + 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.2")) + + By("And descriptor.component.resources should be correct") + Expect(descriptor.Resources).To(HaveLen(2)) + resource := descriptor.Resources[1] + Expect(resource.Name).To(Equal("default-cr")) + Expect(resource.Relation).To(Equal(ocmmetav1.LocalRelation)) + Expect(resource.Type).To(Equal("directory")) + Expect(resource.Version).To(Equal("1.0.2")) + + By("And descriptor.component.resources[1].access should be correct") + defaultCRResourceAccessSpec, err := ocm.DefaultContext().AccessSpecForSpec(descriptor.Resources[1].Access) + Expect(err).ToNot(HaveOccurred()) + defaultCRAccessSpec, ok := defaultCRResourceAccessSpec.(*localblob.AccessSpec) + Expect(ok).To(BeTrue()) + Expect(defaultCRAccessSpec.GetType()).To(Equal(localblob.Type)) + Expect(defaultCRAccessSpec.LocalReference).To(ContainSubstring("sha256:")) + Expect(defaultCRAccessSpec.MediaType).To(Equal("application/x-tar")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with valid module-config containing security-scanner-config and different version, and the git-remote flag", func() { + cmd = createCmd{ + moduleConfigFile: withSecurityConfig, + registry: ociRegistry, + insecure: true, + output: templateOutputPath, + gitRemote: gitRemote, + } + }) + 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 descriptor.component.resources should be correct") + Expect(descriptor.Resources).To(HaveLen(2)) + resource := descriptor.Resources[0] + Expect(resource.Name).To(Equal("template-operator")) + Expect(resource.Relation).To(Equal(ocmmetav1.ExternalRelation)) + Expect(resource.Type).To(Equal("ociArtifact")) + Expect(resource.Version).To(Equal("1.0.0")) + resource = descriptor.Resources[1] + Expect(resource.Name).To(Equal("raw-manifest")) + Expect(resource.Version).To(Equal("1.0.3")) + + By("And descriptor.component.resources[0].access should be correct") + resourceAccessSpec0, err := ocm.DefaultContext().AccessSpecForSpec(descriptor.Resources[0].Access) + Expect(err).ToNot(HaveOccurred()) + ociArtifactAccessSpec, ok := resourceAccessSpec0.(*ociartifact.AccessSpec) + Expect(ok).To(BeTrue()) + Expect(ociArtifactAccessSpec.GetType()).To(Equal(ociartifact.Type)) + Expect(ociArtifactAccessSpec.ImageReference).To(Equal("europe-docker.pkg.dev/kyma-project/prod/template-operator:1.0.0")) + + By("And descriptor.component.resources[1].access should be correct") + resourceAccessSpec1, err := ocm.DefaultContext().AccessSpecForSpec(descriptor.Resources[1].Access) + Expect(err).ToNot(HaveOccurred()) + localBlobAccessSpec, ok := resourceAccessSpec1.(*localblob.AccessSpec) + Expect(ok).To(BeTrue()) + Expect(localBlobAccessSpec.GetType()).To(Equal(localblob.Type)) + Expect(localBlobAccessSpec.LocalReference).To(ContainSubstring("sha256:")) + Expect(localBlobAccessSpec.MediaType).To(Equal("application/x-tar")) + + By("And descriptor.component.sources should be correct") + Expect(len(descriptor.Sources)).To(Equal(1)) + source := descriptor.Sources[0] + sourceAccessSpec, err := ocm.DefaultContext().AccessSpecForSpec(source.Access) + Expect(err).ToNot(HaveOccurred()) + githubAccessSpec, ok := sourceAccessSpec.(*github.AccessSpec) + Expect(ok).To(BeTrue()) + Expect(github.Type).To(Equal(githubAccessSpec.Type)) + Expect(githubAccessSpec.RepoURL).To(Equal("https://github.com/kyma-project/template-operator")) + + By("And spec.mandatory should be false") + Expect(template.Spec.Mandatory).To(BeFalse()) + + By("And security scan labels should be correct") + secScanLabels := flatten(descriptor.Sources[0].Labels) + Expect(secScanLabels).To(HaveKeyWithValue("git.kyma-project.io/ref", "HEAD")) + Expect(secScanLabels).To(HaveKeyWithValue("scan.security.kyma-project.io/rc-tag", "1.0.0")) + Expect(secScanLabels).To(HaveKeyWithValue("scan.security.kyma-project.io/language", "golang-mod")) + Expect(secScanLabels).To(HaveKeyWithValue("scan.security.kyma-project.io/dev-branch", "main")) + Expect(secScanLabels).To(HaveKeyWithValue("scan.security.kyma-project.io/subprojects", "false")) + Expect(secScanLabels).To(HaveKeyWithValue("scan.security.kyma-project.io/exclude", + "**/test/**,**/*_test.go")) + }) + }) + + Context("Given 'modulectl create' command", func() { + var cmd createCmd + It("When invoked with valid module-config containing mandatory true and different version", func() { + cmd = createCmd{ + moduleConfigFile: withMandatoryConfig, + 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.4")) + + By("And spec.mandatory should be true") + Expect(template.Spec.Mandatory).To(BeTrue()) + }) + }) +}) + +// Test helper functions + +func readModuleTemplate(filepath string) (*v1beta2.ModuleTemplate, error) { + moduleTemplate := &v1beta2.ModuleTemplate{} + moduleFile, err := os.ReadFile(filepath) + if err != nil && len(moduleFile) > 0 { + return nil, err + } + err = yaml.Unmarshal(moduleFile, moduleTemplate) + if err != nil { + return nil, err + } + return moduleTemplate, err +} + +func getDescriptor(template *v1beta2.ModuleTemplate) *v1beta2.Descriptor { + if template.Spec.Descriptor.Object != nil { + desc, ok := template.Spec.Descriptor.Object.(*v1beta2.Descriptor) + if !ok || desc == nil { + return nil + } + return desc + } + ocmDesc, err := compdesc.Decode( + template.Spec.Descriptor.Raw, + []compdesc.DecodeOption{compdesc.DisableValidation(true)}...) + if err != nil { + return nil + } + template.Spec.Descriptor.Object = &v1beta2.Descriptor{ComponentDescriptor: ocmDesc} + desc, ok := template.Spec.Descriptor.Object.(*v1beta2.Descriptor) + if !ok { + return nil + } + + return desc +} + +func flatten(labels ocmmetav1.Labels) map[string]string { + labelsMap := make(map[string]string) + for _, l := range labels { + var value string + _ = yaml.Unmarshal(l.Value, &value) + labelsMap[l.Name] = value + } + return labelsMap +} + +func filesIn(dir string) []string { + fi, err := os.Stat(dir) + Expect(err).ToNot(HaveOccurred()) + Expect(fi.IsDir()).To(BeTrueBecause("The provided path should be a directory: %s", dir)) + + dirFs := os.DirFS(dir) + entries, err := fs.ReadDir(dirFs, ".") + Expect(err).ToNot(HaveOccurred()) + + var res []string + for _, ent := range entries { + if ent.Type().IsRegular() { + res = append(res, ent.Name()) + } + } + + return res +} diff --git a/tests/e2e/create/testdata/moduleconfig/invalid/missing-channel.yaml b/tests/e2e/create/testdata/moduleconfig/invalid/missing-channel.yaml new file mode 100644 index 00000000..13f18142 --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/invalid/missing-channel.yaml @@ -0,0 +1,3 @@ +name: kyma-project.io/module/template-operator +version: 1.0.1 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml diff --git a/tests/e2e/create/testdata/moduleconfig/invalid/missing-manifest.yaml b/tests/e2e/create/testdata/moduleconfig/invalid/missing-manifest.yaml new file mode 100644 index 00000000..f97ebd04 --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/invalid/missing-manifest.yaml @@ -0,0 +1,3 @@ +name: kyma-project.io/module/template-operator +channel: regular +version: 1.0.1 diff --git a/tests/e2e/create/testdata/moduleconfig/invalid/missing-name.yaml b/tests/e2e/create/testdata/moduleconfig/invalid/missing-name.yaml new file mode 100644 index 00000000..88697720 --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/invalid/missing-name.yaml @@ -0,0 +1,3 @@ +channel: regular +version: 1.0.1 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml diff --git a/tests/e2e/create/testdata/moduleconfig/invalid/missing-version.yaml b/tests/e2e/create/testdata/moduleconfig/invalid/missing-version.yaml new file mode 100644 index 00000000..73f440db --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/invalid/missing-version.yaml @@ -0,0 +1,3 @@ +name: kyma-project.io/module/template-operator +channel: regular +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml diff --git a/tests/e2e/create/testdata/moduleconfig/valid/minimal.yaml b/tests/e2e/create/testdata/moduleconfig/valid/minimal.yaml new file mode 100644 index 00000000..97ae1545 --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/valid/minimal.yaml @@ -0,0 +1,4 @@ +name: kyma-project.io/module/template-operator +channel: regular +version: 1.0.0 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml diff --git a/tests/e2e/create/testdata/moduleconfig/valid/with-annotations.yaml b/tests/e2e/create/testdata/moduleconfig/valid/with-annotations.yaml new file mode 100644 index 00000000..a81f4a88 --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/valid/with-annotations.yaml @@ -0,0 +1,6 @@ +name: kyma-project.io/module/template-operator +channel: regular +version: 1.0.1 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml +annotations: + operator.kyma-project.io/doc-url: https://kyma-project.io diff --git a/tests/e2e/create/testdata/moduleconfig/valid/with-defaultcr.yaml b/tests/e2e/create/testdata/moduleconfig/valid/with-defaultcr.yaml new file mode 100644 index 00000000..791579bb --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/valid/with-defaultcr.yaml @@ -0,0 +1,5 @@ +name: kyma-project.io/module/template-operator +channel: regular +version: 1.0.2 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml +defaultCR: https://github.com/kyma-project/template-operator/releases/download/1.0.1/default-sample-cr.yaml diff --git a/tests/e2e/create/testdata/moduleconfig/valid/with-mandatory.yaml b/tests/e2e/create/testdata/moduleconfig/valid/with-mandatory.yaml new file mode 100644 index 00000000..c35504c3 --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/valid/with-mandatory.yaml @@ -0,0 +1,5 @@ +name: kyma-project.io/module/template-operator +channel: regular +version: 1.0.4 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml +mandatory: true diff --git a/tests/e2e/create/testdata/moduleconfig/valid/with-security.yaml b/tests/e2e/create/testdata/moduleconfig/valid/with-security.yaml new file mode 100644 index 00000000..f2bfe885 --- /dev/null +++ b/tests/e2e/create/testdata/moduleconfig/valid/with-security.yaml @@ -0,0 +1,5 @@ +name: kyma-project.io/module/template-operator +channel: regular +version: 1.0.3 +manifest: https://github.com/kyma-project/template-operator/releases/download/1.0.1/template-operator.yaml +security: sec-scanners-config.yaml diff --git a/tests/e2e/scaffold/scaffold_suite_test.go b/tests/e2e/scaffold/scaffold_suite_test.go index e6ebe3f6..a666607d 100644 --- a/tests/e2e/scaffold/scaffold_suite_test.go +++ b/tests/e2e/scaffold/scaffold_suite_test.go @@ -1,13 +1,167 @@ +//go:build e2e + package scaffold_test import ( + "fmt" + "os/exec" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) -func TestCreateScaffold(t *testing.T) { +func Test_Scaffold(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "CreateScaffold Suite") + RunSpecs(t, "'Scaffold' Command Test Suite") +} + +// Command wrapper for `modulectl scaffold` + +type scaffoldCmd struct { + moduleName string + moduleVersion string + moduleChannel string + moduleConfigFileFlag string + genDefaultCRFlag string + genSecurityScannersConfigFlag string + genManifestFlag string + overwrite bool +} + +func (cmd *scaffoldCmd) execute() error { + var command *exec.Cmd + + args := []string{"scaffold"} + + if cmd.moduleName != "" { + args = append(args, "--module-name="+cmd.moduleName) + } + + if cmd.moduleVersion != "" { + args = append(args, "--module-version="+cmd.moduleVersion) + } + + if cmd.moduleChannel != "" { + args = append(args, "--module-channel="+cmd.moduleChannel) + } + + if cmd.moduleConfigFileFlag != "" { + args = append(args, "--module-config="+cmd.moduleConfigFileFlag) + } + + if cmd.genDefaultCRFlag != "" { + args = append(args, "--gen-default-cr="+cmd.genDefaultCRFlag) + } + + if cmd.genSecurityScannersConfigFlag != "" { + args = append(args, "--gen-security-config="+cmd.genSecurityScannersConfigFlag) + } + + if cmd.genManifestFlag != "" { + args = append(args, "--gen-manifest="+cmd.genManifestFlag) + } + + if cmd.overwrite { + args = append(args, "--overwrite=true") + } + + command = exec.Command("modulectl", args...) + cmdOut, err := command.CombinedOutput() + if err != nil { + return fmt.Errorf("scaffold command failed with output: %s and error: %w", cmdOut, err) + } + return nil +} + +func (cmd *scaffoldCmd) toConfigBuilder() *moduleConfigBuilder { + res := &moduleConfigBuilder{} + res.defaults() + if cmd.moduleName != "" { + res.withName(cmd.moduleName) + } + if cmd.moduleVersion != "" { + res.withVersion(cmd.moduleVersion) + } + if cmd.moduleChannel != "" { + res.withChannel(cmd.moduleChannel) + } + if cmd.genDefaultCRFlag != "" { + res.withDefaultCRPath(cmd.genDefaultCRFlag) + } + if cmd.genSecurityScannersConfigFlag != "" { + res.withSecurityScannersPath(cmd.genSecurityScannersConfigFlag) + } + if cmd.genManifestFlag != "" { + res.withManifestPath(cmd.genManifestFlag) + } + return res +} + +// moduleConfigBuilder is used to simplify module.Config creation for testing purposes +type moduleConfigBuilder struct { + moduleConfig +} + +func (mcb *moduleConfigBuilder) get() *moduleConfig { + res := mcb.moduleConfig + return &res +} + +func (mcb *moduleConfigBuilder) withName(val string) *moduleConfigBuilder { + mcb.Name = val + return mcb +} + +func (mcb *moduleConfigBuilder) withVersion(val string) *moduleConfigBuilder { + mcb.Version = val + return mcb +} + +func (mcb *moduleConfigBuilder) withChannel(val string) *moduleConfigBuilder { + mcb.Channel = val + return mcb +} + +func (mcb *moduleConfigBuilder) withManifestPath(val string) *moduleConfigBuilder { + mcb.ManifestPath = val + return mcb +} + +func (mcb *moduleConfigBuilder) withDefaultCRPath(val string) *moduleConfigBuilder { + mcb.DefaultCRPath = val + return mcb +} + +func (mcb *moduleConfigBuilder) withSecurityScannersPath(val string) *moduleConfigBuilder { + mcb.Security = val + return mcb +} + +func (mcb *moduleConfigBuilder) defaults() *moduleConfigBuilder { + return mcb. + withName("kyma-project.io/module/mymodule"). + withVersion("0.0.1"). + withChannel("regular"). + withManifestPath("manifest.yaml") +} + +// This is a copy of the moduleConfig struct from internal/scaffold/contentprovider/moduleconfig.go +// to not make the moduleConfig public just for the sake of testing. +// It is expected that the moduleConfig struct will be made public in the future when introducing more commands. +// Once it is public, this struct should be removed. +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"` } diff --git a/tests/e2e/scaffold/scaffold_test.go b/tests/e2e/scaffold/scaffold_test.go index 2d5dc32a..d20e17e6 100644 --- a/tests/e2e/scaffold/scaffold_test.go +++ b/tests/e2e/scaffold/scaffold_test.go @@ -1,10 +1,10 @@ +//go:build e2e + package scaffold_test import ( - "fmt" "io/fs" "os" - "os/exec" "path" "gopkg.in/yaml.v3" @@ -17,7 +17,7 @@ const ( markerFileData = "test-marker" ) -var _ = Describe("Scaffold Command", Ordered, func() { +var _ = Describe("Test 'scaffold' command", Ordered, func() { var initialDir string var workDir string @@ -206,6 +206,8 @@ var _ = Describe("Scaffold Command", Ordered, func() { }) }) +// Test helper functions + func getMarkerFileData(name string) string { data, err := os.ReadFile(name) Expect(err).ToNot(HaveOccurred()) @@ -264,151 +266,3 @@ func cleanupWorkingDirectory(path string) { _ = os.RemoveAll(path) } } - -type scaffoldCmd struct { - moduleName string - moduleVersion string - moduleChannel string - moduleConfigFileFlag string - genDefaultCRFlag string - genSecurityScannersConfigFlag string - genManifestFlag string - overwrite bool -} - -func (cmd *scaffoldCmd) execute() error { - var command *exec.Cmd - - args := []string{"scaffold"} - - if cmd.moduleName != "" { - args = append(args, "--module-name="+cmd.moduleName) - } - - if cmd.moduleVersion != "" { - args = append(args, "--module-version="+cmd.moduleVersion) - } - - if cmd.moduleChannel != "" { - args = append(args, "--module-channel="+cmd.moduleChannel) - } - - if cmd.moduleConfigFileFlag != "" { - args = append(args, "--module-config="+cmd.moduleConfigFileFlag) - } - - if cmd.genDefaultCRFlag != "" { - args = append(args, "--gen-default-cr="+cmd.genDefaultCRFlag) - } - - if cmd.genSecurityScannersConfigFlag != "" { - args = append(args, "--gen-security-config="+cmd.genSecurityScannersConfigFlag) - } - - if cmd.genManifestFlag != "" { - args = append(args, "--gen-manifest="+cmd.genManifestFlag) - } - - if cmd.overwrite { - args = append(args, "--overwrite=true") - } - - command = exec.Command("modulectl", args...) - cmdOut, err := command.CombinedOutput() - if err != nil { - return fmt.Errorf("scaffold command failed with output: %s and error: %w", cmdOut, err) - } - return nil -} - -func (cmd *scaffoldCmd) toConfigBuilder() *moduleConfigBuilder { - res := &moduleConfigBuilder{} - res.defaults() - if cmd.moduleName != "" { - res.withName(cmd.moduleName) - } - if cmd.moduleVersion != "" { - res.withVersion(cmd.moduleVersion) - } - if cmd.moduleChannel != "" { - res.withChannel(cmd.moduleChannel) - } - if cmd.genDefaultCRFlag != "" { - res.withDefaultCRPath(cmd.genDefaultCRFlag) - } - if cmd.genSecurityScannersConfigFlag != "" { - res.withSecurityScannersPath(cmd.genSecurityScannersConfigFlag) - } - if cmd.genManifestFlag != "" { - res.withManifestPath(cmd.genManifestFlag) - } - return res -} - -// moduleConfigBuilder is used to simplify module.Config creation for testing purposes -type moduleConfigBuilder struct { - moduleConfig -} - -func (mcb *moduleConfigBuilder) get() *moduleConfig { - res := mcb.moduleConfig - return &res -} - -func (mcb *moduleConfigBuilder) withName(val string) *moduleConfigBuilder { - mcb.Name = val - return mcb -} - -func (mcb *moduleConfigBuilder) withVersion(val string) *moduleConfigBuilder { - mcb.Version = val - return mcb -} - -func (mcb *moduleConfigBuilder) withChannel(val string) *moduleConfigBuilder { - mcb.Channel = val - return mcb -} - -func (mcb *moduleConfigBuilder) withManifestPath(val string) *moduleConfigBuilder { - mcb.ManifestPath = val - return mcb -} - -func (mcb *moduleConfigBuilder) withDefaultCRPath(val string) *moduleConfigBuilder { - mcb.DefaultCRPath = val - return mcb -} - -func (mcb *moduleConfigBuilder) withSecurityScannersPath(val string) *moduleConfigBuilder { - mcb.Security = val - return mcb -} - -func (mcb *moduleConfigBuilder) defaults() *moduleConfigBuilder { - return mcb. - withName("kyma-project.io/module/mymodule"). - withVersion("0.0.1"). - withChannel("regular"). - withManifestPath("manifest.yaml") -} - -// This is a copy of the moduleConfig struct from internal/scaffold/contentprovider/moduleconfig.go -// to not make the moduleConfig public just for the sake of testing. -// It is expected that the moduleConfig struct will be made public in the future when introducing more commands. -// Once it is public, this struct should be removed. -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"` -} diff --git a/unit-test-coverage.yaml b/unit-test-coverage.yaml index 9a656c9b..80773ba3 100644 --- a/unit-test-coverage.yaml +++ b/unit-test-coverage.yaml @@ -1,7 +1,7 @@ packages: cmd/modulectl/scaffold: 100 internal/common/validation: 86 - internal/service/scaffold: 93 + internal/service/scaffold: 92 internal/service/contentprovider: 100 internal/service/filegenerator: 100 internal/service/moduleconfig/generator: 100