Compare commits

...

6 Commits

Author SHA1 Message Date
Amit Singh fe9631b038
Merge f16fe41810 into 305a90f428 2025-11-03 08:56:39 +05:30
AshvinBambhaniya2003 305a90f428
Feat(addon): Store addon registry tokens in Secrets (#6935)
CodeQL / Analyze (go) (push) Has been cancelled Details
Definition-Lint / definition-doc (push) Has been cancelled Details
E2E MultiCluster Test / detect-noop (push) Has been cancelled Details
E2E Test / detect-noop (push) Has been cancelled Details
Go / detect-noop (push) Has been cancelled Details
license / Check for unapproved licenses (push) Has been cancelled Details
Registry / Build and Push Vela Images (push) Has been cancelled Details
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled Details
Unit-Test / detect-noop (push) Has been cancelled Details
Webhook Upgrade Validation / webhook-upgrade-check (push) Has been cancelled Details
E2E MultiCluster Test / e2e-multi-cluster-tests (v1.31.9) (push) Has been cancelled Details
E2E Test / e2e-tests (v1.31) (push) Has been cancelled Details
Go / staticcheck (push) Has been cancelled Details
Go / lint (push) Has been cancelled Details
Go / check-diff (push) Has been cancelled Details
Go / check-windows (push) Has been cancelled Details
Go / check-core-image-build (push) Has been cancelled Details
Go / check-cli-image-build (push) Has been cancelled Details
Registry / Generate and Push Provenance to GCHR (${{ needs.publish-vela-images.outputs.vela_cli_digest }}, ${{ needs.publish-vela-images.outputs.vela_cli_image }}, Vela CLI Image) (push) Has been cancelled Details
Registry / Generate and Push Provenance to GCHR (${{ needs.publish-vela-images.outputs.vela_core_digest }}, ${{ needs.publish-vela-images.outputs.vela_core_image }}, Vela Core Image) (push) Has been cancelled Details
Registry / Generate and Push Provenance to DockerHub (${{ needs.publish-vela-images.outputs.vela_cli_digest }}, ${{ needs.publish-vela-images.outputs.vela_cli_dockerhub_image }}, Vela CLI Image) (push) Has been cancelled Details
Registry / Generate and Push Provenance to DockerHub (${{ needs.publish-vela-images.outputs.vela_core_digest }}, ${{ needs.publish-vela-images.outputs.vela_core_dockerhub_image }}, Vela Core Image) (push) Has been cancelled Details
Unit-Test / unit-tests (push) Has been cancelled Details
* feat(addon): Store addon registry tokens in Secrets

Previously, addon registry tokens were stored in plaintext within the 'vela-addon-registry' ConfigMap. This is not a secure practice for sensitive data.

This commit refactors the addon registry functionality to store tokens in Kubernetes Secrets. The ConfigMap now only contains a reference to the secret name, while the token itself is stored securely.

This change includes:
- Creating/updating secrets when a registry is added/updated.
- Loading tokens from secrets when a registry is listed/retrieved.
- Deleting secrets when a registry is deleted.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* test(addon): Add tests for registry token secret storage

This commit introduces a comprehensive test suite for the addon registry feature.

It includes:
- Isolated unit tests for each CRUD operation (Add, Update, List, Get, Delete) to ensure each function works correctly in isolation.
- A stateful integration test to validate the complete lifecycle of an addon registry from creation to deletion.

The tests verify that tokens are handled correctly via Kubernetes Secrets, confirming the implementation of the secure token storage feature.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* feat(addon): improve addon registry robustness and fix bugs

This commit introduces several improvements to the addon registry to make it more robust and fixes several bugs.

- When updating a secret, the existing secret is now fetched and updated to avoid potential conflicts.
- Deleting a non-existent registry now returns no error, making the operation idempotent.
- Getting a non-existent registry now returns a structured not-found error.
- Loading a token from a non-existent secret is now handled gracefully.
- When setting a token directly on a git-based addon source, the token secret reference is now cleared.
- The token secret reference is now correctly copied in `SafeCopy`.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* Refactor(addon): Fix secret deletion and improve registry logic

This commit refactors the addon registry data store to fix a critical bug where deleting an addon registry would not delete its associated token secret.

The root cause was that the `GetRegistry` function, which was used by `DeleteRegistry`, would load the token from the secret and then clear the `TokenSecretRef` field on the in-memory object. This meant that when `DeleteRegistry` tried to find the secret to delete, the reference was already gone.

This has been fixed by:
1. Introducing a central `getRegistries` helper function to read the raw registry data from the ConfigMap.
2. Refactoring all data store methods (`List`, `Get`, `Add`, `Update`, `Delete`) to use this central helper, removing duplicate code.
3. Ensuring `DeleteRegistry` uses the raw, unmodified registry data so that the `TokenSecretRef` is always available for deletion.

Additionally, comprehensive unit tests for the new helper functions (`getRegistries`, `loadTokenFromSecret`, `createOrUpdateTokenSecret`) have been added to verify the fix and improve overall code quality and stability.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* feat(addon): improve addon registry token security and logging

This commit enhances the security and observability of addon registry token handling.

- Adds a warning message to users when an insecure inline token is detected in an addon registry configuration, prompting them to migrate to a more secure secret-based storage.
- Implements info-level logging to create an audit trail for token migrations, providing administrators with visibility into security-related events.
- Refactors the token migration logic into a new `migrateInlineTokenToSecret` function, improving code clarity and maintainability.
- Introduces unit tests for the `TokenSource` interface methods and the `GetTokenSource` function to ensure correctness and prevent regressions.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* Chore: remove comments to triger ci

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

---------

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>
2025-10-31 13:52:30 +00:00
AshvinBambhaniya2003 d1f077ee0d
Fix(addon): show correct owner in definition conflict error (#6903)
* fix(addon): show correct owner in definition conflict error

When enabling an addon, if a definition conflicted with one from another existing addon, the error message would misleadingly cite the addon being installed as the owner, rather than the actual owner of the definition. This made it difficult for users to diagnose the conflict.

This commit corrects the error message generation in `checkConflictDefs` to use the name of the actual owner application. A comprehensive unit test for this function has also been added to verify the corrected behavior and prevent regressions.

Fixes #6898

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* fix(addon): show correct owner name in conflict message

When a definition conflict occurs, the error message attempts to show the addon that owns the existing definition.

However, if the owner is not a KubeVela addon application (i.e., its name doesn't have the 'addon-' prefix), the `AppName2Addon` function returns an empty string. This resulted in a confusing conflict message with a blank owner name, like "already exist in  \n".

This patch fixes the issue by checking if the result of `AppName2Addon` is empty. If it is, it falls back to using the full application name of the owner,
ensuring the conflict message is always clear and actionable.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* chore(addon): update comment for addon name

- Add this comment to trigger ci

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* fix(addon): improve conflict message for addon definitions

adjust comment placement and logic to ensure correct addon name display in conflict messages

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

---------

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>
2025-10-31 13:52:00 +00:00
AshvinBambhaniya2003 260fc1a294
Feat: Enhance unit test coverage for `references/appfile` package (#6913)
* feat(appfile): Enhance unit test coverage and migrate to standard Go testing

This commit significantly enhances the unit test coverage for the `references/appfile` package by introducing a comprehensive suite of new test cases and migrating existing tests to the standard Go `testing` framework with `testify/assert`.

Key additions and improvements include:
- **New Test Cases for `references/appfile/api/appfile.go`**: Added tests for `NewAppFile`, `JSONToYaml`, and `LoadFromBytes` to ensure correct application file initialization, parsing, and loading.
- **New Test Cases for `references/appfile/api/service.go`**: Introduced tests for `GetUserConfigName`, `GetApplicationConfig`, and `ToStringSlice` to validate service configuration extraction and type conversions.
- **Expanded Test Coverage for `references/appfile/app.go`**: Added new tests for `NewApplication`, `Validate`, `GetComponents`, `GetServiceConfig`, `GetApplicationSettings`, `GetWorkload`, and `GetTraits`, ensuring the robustness of application-level operations.
- **Dedicated Test Files for `modify.go` and `run.go`**: Created `modify_test.go` and `run_test.go` to provide specific unit tests for `SetWorkload`, `CreateOrUpdateApplication`, `CreateOrUpdateObjects`, and `Run` functions.
- **Test Framework Migration**: Refactored `addon_suit_test.go` to `main_test.go` and `addon_test.go` to use standard Go `testing` and `testify/assert`, improving consistency and maintainability.

These changes collectively improve the robustness, reliability, and maintainability of the `appfile` package by providing a more comprehensive and standardized testing approach.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* chore(references/appfile): improve test suite robustness and style

This commit introduces two improvements to the test suite in the `references/appfile` package.

First, the `TestMain` function in `main_test.go` is refactored to ensure the `envtest` control-plane is always stopped, even if test setup fails. This is achieved by creating a single exit path that handles cleanup, preventing resource leaks.

Second, a minor linting issue (S1005) in `modify_test.go` is fixed by removing an unnecessary assignment to the blank identifier.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* Chore: remove comment to trigger ci

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

---------

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>
2025-10-31 13:51:09 +00:00
AshvinBambhaniya2003 24f6718619
Feat(testing): Enhance Unit Test Coverage for Core Utility Packages (#6929)
* test(cli): enhance unit test coverage for theme and color config

This commit introduces a comprehensive suite of unit tests for the theme and color configuration functions in `references/cli/top/config`.

Key changes include:
- Refactored existing tests in `color_test.go` to use table-driven sub-tests for improved clarity and maintainability.
- Added new test functions to validate color parsing, hex color detection, and default theme creation.
- Implemented tests for theme file lifecycle management, including creation and loading logic.

These additions significantly increase the test coverage and ensure the robustness and correctness of the CLI's theme and color functionality.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* test(cli): refactor and enhance tests for top view models and utils

This commit improves the unit test suite for the CLI's top view functionality by refactoring existing tests and adding new ones to increase coverage.

Key changes include:
- In `application_test.go`, `TestApplicationList_ToTableBody` is refactored to be a table-driven test, and new tests are added for `serviceNum`, `workflowMode`, and `workflowStepNum` helpers.
- In `time_test.go`, `TestTimeFormat` is refactored into a table-driven test for better structure and readability.

These changes align the tests with best practices and improve the overall robustness of the CLI top view's data presentation logic.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* test(cuegen): enhance unit test coverage for CUE generation packages

This commit introduces a comprehensive suite of unit tests and refactors existing tests for the CUE generation packages located in `references/cuegen`.

Key changes include:
- Refactored existing tests in `generator_test.go` and `provider_test.go` to use table-driven sub-tests, improving clarity, maintainability, and coverage of error conditions.
- Added new test functions to `convert_test.go` to validate helper functions for comment generation, type support, and enum field handling.
- Added new tests in `provider_test.go` to cover provider extraction, declaration modification, and panic recovery logic.

These changes significantly increase the test coverage for the `cuegen` libraries, ensuring the correctness and robustness of the CUE code generation functionality.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* test(docgen): add comprehensive unit tests for doc generation

This commit introduces a comprehensive suite of unit tests for the documentation generation package located in `references/docgen`.

Key changes include:
- Added new test files (`console_test.go`, `convert_test.go`, `openapi_test.go`) to cover the core functions for parsing and generating documentation for CUE, Terraform, and OpenAPI schemas.
- Refactored and enhanced `i18n_test.go` to use sub-tests, resolve race conditions, and improve coverage for fallback logic and error handling.
- Ensured all new and existing tests follow best practices, using table-driven tests for clarity and maintainability.

This effort significantly increases the test coverage for the `docgen` package, improving the reliability and robustness of the documentation generation features.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* test: improve test reliability and conventions

This commit introduces several improvements to the test suite to enhance reliability and adhere to best practices.

- **Fix flaky test in `docgen/openapi_test.go`**:
  The test for `GenerateConsoleDocument` was flaky because it performed an exact string match on table output generated from a map. Since map iteration order is not guaranteed, this could cause spurious failures. The test is now order-insensitive, comparing sorted sets of lines instead.

- **Improve assertions in `docgen/console_test.go`**:
  - Removes an unnecessary `test.EquateErrors()` option, which is not needed for simple string comparisons.
  - Corrects the `cmp.Diff` argument order to the standard `(want, got)` convention for clearer failure messages.
  - Fixes a typo in an error message.

- **Standardize assertions in `cli/top/config/color_test.go`**:
  Swaps `assert.Equal` arguments to the standard `(expected, actual)` convention.

- **Clean up `cuegen/generators/provider/provider_test.go`**:
  Removes a redundant error check.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

---------

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>
2025-10-31 13:50:30 +00:00
AshvinBambhaniya2003 44ac92d1ba
Feat(test): Enhance unit test coverage for webhook, workflow, VELAQL, and monitor packages (#6895)
* feat(monitor): Add unit tests for application metrics watcher

This commit introduces a new test file with comprehensive unit tests for the application metrics watcher functionality in pkg/monitor/watcher.

Key additions include:
  - Test cases for the application metrics watcher's inc() method covering add, delete, and update operations
  - Test cases for report() method that verifies dirty flags are cleared
  - Test cases for helper functions getPhase() and getApp()

These additions improve the overall test coverage and ensure the correctness of the application metrics monitoring functionality.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* feat(velaql): Add comprehensive unit tests for ParseVelaQLFromPath

This commit introduces new unit tests for the ParseVelaQLFromPath function in pkg/velaql, along with test data files to improve test coverage and ensure correctness.

Key additions include:
  - `pkg/velaql/parse_test.go`: Adds TestParseVelaQLFromPath function with comprehensive test cases covering:
    * Valid CUE files with and without export fields
    * Nonexistent and empty file paths
    * Invalid CUE content
    * Files with invalid export types
  - Test data files in pkg/velaql/testdata/:
    * simple-valid.cue: Valid CUE file with export field
    * simple-no-export.cue: Valid CUE file without export field
    * empty.cue: Empty CUE file
    * invalid-cue-content.cue: CUE file with invalid syntax
    * invalid-export.cue: CUE file with invalid export type

These additions improve the overall test coverage and ensure the robustness of the VELAQL parsing functionality.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* feat(webhook): Add unit tests for ValidateDefinitionRevision function

This commit introduces new unit tests for the ValidateDefinitionRevision function in pkg/webhook/utils to improve test coverage and ensure correctness of definition revision validation.

Key additions include:
  - `pkg/webhook/utils/utils_test.go`: Adds TestValidateDefinitionRevision function with comprehensive test cases covering:
    * Success scenarios with matching definition revisions
    * Success scenarios when definition revision does not exist
    * Failure scenarios with revision hash mismatches
    * Failure scenarios with spec mismatches
    * Failure scenarios with invalid definition revision names

These additions improve the overall test coverage and ensure the robustness of the webhook utility functions for validating definition revisions.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* feat(workflow): Add unit tests for OAM apply and query utilities

This commit introduces new unit tests for workflow provider functions in pkg/workflow/providers to improve test coverage and ensure correctness.

Key additions include:
  - `pkg/workflow/providers/oam/apply_test.go`: Adds TestRenderComponent function with comprehensive test cases for component rendering
  - `pkg/workflow/providers/query/utils_test.go`: Adds:
    * TestBuildResourceArray function with comprehensive test cases covering simple, nested, and complex resource tree scenarios
    * TestBuildResourceItem function with test cases for resources with and without annotations

These additions improve the overall test coverage and ensure the robustness of the workflow provider functions for OAM applications.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* fix(velaql): Improve error handling in ParseVelaQLFromPath test

This commit addresses an issue in the TestParseVelaQLFromPath function where file read errors were being silently ignored. The changes include:

- Removing the unused expectedView field from test cases
- Replacing conditional error checking with require.NoError to ensure file read operations are properly validated
- Ensuring that test failures are properly reported when file reading fails

This fix improves the reliability of the test suite by making sure that any file I/O errors are properly caught and reported rather than silently ignored.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* feat: Apply cross-cutting test improvements

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* feat: Enhance test coverage with file-specific suggestions

This commit applies file-specific suggestions to enhance the test suite's
coverage and robustness.

Key changes include:

- **`pkg/monitor/watcher/application_test.go`**:
  - Added a test case for a multi-step workflow with mixed phases to
    validate `stepPhaseCounter` aggregation.
  - Added a test for idempotence by calling `inc` twice.
  - Added test cases for an empty workflow and an unknown application phase.
  - Strengthened the `report` test to assert that counters are not cleared.

- **`pkg/velaql/parse_test.go`**:
  - Added a test case for `ParseVelaQLFromPath` to handle files with
    leading/trailing whitespace.
  - Added a test case to ensure consistent error messages for relative paths.

- **`pkg/webhook/utils/utils_test.go`**:
  - Added a test case to `TestValidateCueTemplate` for a malformed CUE
    template.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

---------

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>
2025-10-31 13:49:15 +00:00
36 changed files with 4247 additions and 512 deletions

View File

@ -24,14 +24,43 @@ import (
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
velatypes "github.com/oam-dev/kubevela/apis/types"
)
const registryConfigMapName = "vela-addon-registry"
const registriesKey = "registries"
const tokenSecretNamePrefix = "addon-registry-"
// TokenSource is an interface for addon source that has token
type TokenSource interface {
// GetToken return the token of the source
GetToken() string
// SetToken set the token of the source
SetToken(string)
// SetTokenSecretRef set the token secret ref to the source
SetTokenSecretRef(string)
// GetTokenSecretRef return the token secret ref of the source
GetTokenSecretRef() string
}
// GetTokenSource return the token source of the registry
func (r *Registry) GetTokenSource() TokenSource {
if r.Git != nil {
return r.Git
}
if r.Gitee != nil {
return r.Gitee
}
if r.Gitlab != nil {
return r.Gitlab
}
return nil
}
// Registry represent a addon registry model
type Registry struct {
@ -62,28 +91,49 @@ type registryImpl struct {
client client.Client
}
func (r registryImpl) ListRegistries(ctx context.Context) ([]Registry, error) {
// getRegistries is a helper to fetch and unmarshal all registries from the ConfigMap
func (r registryImpl) getRegistries(ctx context.Context) (map[string]Registry, *v1.ConfigMap, error) {
cm := &v1.ConfigMap{}
if err := r.client.Get(ctx, types.NamespacedName{Namespace: velatypes.DefaultKubeVelaNS, Name: registryConfigMapName}, cm); err != nil {
return nil, err
err := r.client.Get(ctx, types.NamespacedName{Namespace: velatypes.DefaultKubeVelaNS, Name: registryConfigMapName}, cm)
if err != nil {
return nil, nil, err
}
if _, ok := cm.Data[registriesKey]; !ok {
return nil, NewAddonError("Error addon registry configmap registry-key not exist")
return nil, nil, NewAddonError("error addon registry configmap registry-key not exist")
}
registries := map[string]Registry{}
if err := json.Unmarshal([]byte(cm.Data[registriesKey]), &registries); err != nil {
return nil, cm, err
}
return registries, cm, nil
}
func (r registryImpl) ListRegistries(ctx context.Context) ([]Registry, error) {
registries, _, err := r.getRegistries(ctx)
if err != nil {
if apierrors.IsNotFound(err) {
return []Registry{}, nil
}
return nil, err
}
var res []Registry
for _, registry := range registries {
if err := loadTokenFromSecret(ctx, r.client, &registry); err != nil {
return nil, err
}
res = append(res, registry)
}
return res, nil
}
func (r registryImpl) AddRegistry(ctx context.Context, registry Registry) error {
cm := &v1.ConfigMap{}
if err := r.client.Get(ctx, types.NamespacedName{Namespace: velatypes.DefaultKubeVelaNS, Name: registryConfigMapName}, cm); err != nil {
if err := createOrUpdateTokenSecret(ctx, r.client, &registry); err != nil {
return err
}
registries, cm, err := r.getRegistries(ctx)
if err != nil {
if apierrors.IsNotFound(err) {
b, err := json.Marshal(map[string]Registry{
registry.Name: registry,
@ -91,7 +141,7 @@ func (r registryImpl) AddRegistry(ctx context.Context, registry Registry) error
if err != nil {
return err
}
cm = &v1.ConfigMap{
cm := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: registryConfigMapName,
Namespace: velatypes.DefaultKubeVelaNS,
@ -104,48 +154,110 @@ func (r registryImpl) AddRegistry(ctx context.Context, registry Registry) error
}
return err
}
registries := map[string]Registry{}
if err := json.Unmarshal([]byte(cm.Data[registriesKey]), &registries); err != nil {
return err
}
registries[registry.Name] = registry
b, err := json.Marshal(registries)
if err != nil {
return err
}
cm.Data = map[string]string{
registriesKey: string(b),
}
cm.Data[registriesKey] = string(b)
return r.client.Update(ctx, cm)
}
// createOrUpdateTokenSecret will create or update a secret to store registry token
func createOrUpdateTokenSecret(ctx context.Context, cli client.Client, registry *Registry) error {
source := registry.GetTokenSource()
if source == nil {
return nil
}
token := source.GetToken()
if token == "" {
return nil
}
return migrateInlineTokenToSecret(ctx, cli, registry, source, token)
}
// migrateInlineTokenToSecret will migrate an inline token to a secret.
// It will take the token from the registry object, create/update a secret, and set the secret ref on the registry object.
func migrateInlineTokenToSecret(ctx context.Context, cli client.Client, registry *Registry, source TokenSource, token string) error {
log := logf.FromContext(ctx)
secretName := tokenSecretNamePrefix + registry.Name
source.SetTokenSecretRef(secretName)
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: velatypes.DefaultKubeVelaNS,
},
Type: v1.SecretTypeOpaque,
Data: map[string][]byte{
"token": []byte(token),
},
}
err := cli.Create(ctx, secret)
if err != nil {
if apierrors.IsAlreadyExists(err) {
existingSecret := &v1.Secret{}
if err := cli.Get(ctx, types.NamespacedName{Name: secretName, Namespace: velatypes.DefaultKubeVelaNS}, existingSecret); err != nil {
return err
}
existingSecret.Data = secret.Data
if err := cli.Update(ctx, existingSecret); err != nil {
return err
}
log.Info("Successfully updated secret for addon registry token", "registry", registry.Name, "secret", secretName)
return nil
}
return err
}
log.Info("Successfully created secret for addon registry token", "registry", registry.Name, "secret", secretName)
return nil
}
func (r registryImpl) DeleteRegistry(ctx context.Context, name string) error {
cm := &v1.ConfigMap{}
if err := r.client.Get(ctx, types.NamespacedName{Namespace: velatypes.DefaultKubeVelaNS, Name: registryConfigMapName}, cm); err != nil {
registries, cm, err := r.getRegistries(ctx)
if err != nil {
if apierrors.IsNotFound(err) {
return nil
}
return err
}
registries := map[string]Registry{}
if err := json.Unmarshal([]byte(cm.Data[registriesKey]), &registries); err != nil {
return err
reg, ok := registries[name]
if !ok {
return nil
}
if source := reg.GetTokenSource(); source != nil {
if secretName := source.GetTokenSecretRef(); secretName != "" {
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: velatypes.DefaultKubeVelaNS,
},
}
if err := r.client.Delete(ctx, secret); err != nil && !apierrors.IsNotFound(err) {
return err
}
}
}
delete(registries, name)
b, err := json.Marshal(registries)
if err != nil {
return err
}
cm.Data = map[string]string{
registriesKey: string(b),
}
cm.Data[registriesKey] = string(b)
return r.client.Update(ctx, cm)
}
func (r registryImpl) UpdateRegistry(ctx context.Context, registry Registry) error {
cm := &v1.ConfigMap{}
if err := r.client.Get(ctx, types.NamespacedName{Namespace: velatypes.DefaultKubeVelaNS, Name: registryConfigMapName}, cm); err != nil {
if err := createOrUpdateTokenSecret(ctx, r.client, &registry); err != nil {
return err
}
registries := map[string]Registry{}
if err := json.Unmarshal([]byte(cm.Data[registriesKey]), &registries); err != nil {
registries, cm, err := r.getRegistries(ctx)
if err != nil {
return err
}
if _, ok := registries[registry.Name]; !ok {
@ -156,25 +268,50 @@ func (r registryImpl) UpdateRegistry(ctx context.Context, registry Registry) err
if err != nil {
return err
}
cm.Data = map[string]string{
registriesKey: string(b),
}
cm.Data[registriesKey] = string(b)
return r.client.Update(ctx, cm)
}
func (r registryImpl) GetRegistry(ctx context.Context, name string) (Registry, error) {
var res Registry
cm := &v1.ConfigMap{}
if err := r.client.Get(ctx, types.NamespacedName{Namespace: velatypes.DefaultKubeVelaNS, Name: registryConfigMapName}, cm); err != nil {
return res, err
registries, _, err := r.getRegistries(ctx)
if err != nil {
return Registry{}, err
}
registries := map[string]Registry{}
if err := json.Unmarshal([]byte(cm.Data[registriesKey]), &registries); err != nil {
return res, err
res, ok := registries[name]
if !ok {
return res, apierrors.NewNotFound(schema.GroupResource{Group: "addons.kubevela.io", Resource: "Registry"}, name)
}
var notExist bool
if res, notExist = registries[name]; !notExist {
return res, fmt.Errorf("registry name %s not found", name)
if err := loadTokenFromSecret(ctx, r.client, &res); err != nil {
return res, err
}
return res, nil
}
// loadTokenFromSecret will load token from secret if exists
// and set it to the source of the registry object
func loadTokenFromSecret(ctx context.Context, cli client.Client, registry *Registry) error {
source := registry.GetTokenSource()
if source == nil {
return nil
}
secretName := source.GetTokenSecretRef()
if secretName == "" {
if source.GetToken() != "" {
// For backward compatibility, token can be stored in configmap directly.
// This is not secure, so we print a warning and recommend user to upgrade.
// The upgrade can be done by editing and saving the addon registry again.
fmt.Printf("Warning: addon registry %s is using an insecure token stored in ConfigMap. Please edit and save this addon registry again to migrate the token to a secret.\n", registry.Name)
}
return nil
}
secret := &v1.Secret{}
if err := cli.Get(ctx, types.NamespacedName{Namespace: velatypes.DefaultKubeVelaNS, Name: secretName}, secret); err != nil {
if apierrors.IsNotFound(err) {
// If the secret is not found, we consider the token is empty
return nil
}
return err
}
source.SetToken(string(secret.Data["token"]))
return nil
}

643
pkg/addon/registry_test.go Normal file
View File

@ -0,0 +1,643 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package addon
import (
"context"
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
velatypes "github.com/oam-dev/kubevela/apis/types"
)
func TestAddonRegistry(t *testing.T) {
ctx := context.Background()
testRegistry := Registry{
Name: "test-registry",
Git: &GitAddonSource{
URL: "http://github.com/test/repo",
Token: "test-token",
},
}
scheme := runtime.NewScheme()
assert.NoError(t, v1.AddToScheme(scheme))
client := fake.NewClientBuilder().WithScheme(scheme).Build()
ds := NewRegistryDataStore(client)
t.Run("add registry", func(t *testing.T) {
err := ds.AddRegistry(ctx, testRegistry)
assert.NoError(t, err)
var cm v1.ConfigMap
err = client.Get(ctx, types.NamespacedName{Name: registryConfigMapName, Namespace: velatypes.DefaultKubeVelaNS}, &cm)
assert.NoError(t, err)
var registries map[string]Registry
err = json.Unmarshal([]byte(cm.Data[registriesKey]), &registries)
assert.NoError(t, err)
assert.Equal(t, 1, len(registries))
gotRegistry := registries["test-registry"]
assert.Equal(t, "", gotRegistry.Git.Token)
assert.Equal(t, "addon-registry-test-registry", gotRegistry.Git.TokenSecretRef)
var secret v1.Secret
err = client.Get(ctx, types.NamespacedName{Name: "addon-registry-test-registry", Namespace: velatypes.DefaultKubeVelaNS}, &secret)
assert.NoError(t, err)
assert.Equal(t, "test-token", string(secret.Data["token"]))
})
t.Run("update registry", func(t *testing.T) {
updatedRegistry := Registry{
Name: "test-registry",
Git: &GitAddonSource{
URL: "http://github.com/test/repo-updated",
Token: "test-token-updated",
},
}
err := ds.UpdateRegistry(ctx, updatedRegistry)
assert.NoError(t, err)
var secret v1.Secret
err = client.Get(ctx, types.NamespacedName{Name: "addon-registry-test-registry", Namespace: velatypes.DefaultKubeVelaNS}, &secret)
assert.NoError(t, err)
assert.Equal(t, "test-token-updated", string(secret.Data["token"]))
})
t.Run("list and get registry", func(t *testing.T) {
registries, err := ds.ListRegistries(ctx)
assert.NoError(t, err)
assert.Equal(t, 1, len(registries))
assert.Equal(t, "test-token-updated", registries[0].Git.Token)
reg, err := ds.GetRegistry(ctx, "test-registry")
assert.NoError(t, err)
assert.Equal(t, "test-token-updated", reg.Git.Token)
})
t.Run("delete registry", func(t *testing.T) {
err := ds.DeleteRegistry(ctx, "test-registry")
assert.NoError(t, err)
var cm v1.ConfigMap
err = client.Get(ctx, types.NamespacedName{Name: registryConfigMapName, Namespace: velatypes.DefaultKubeVelaNS}, &cm)
assert.NoError(t, err)
var registries map[string]Registry
err = json.Unmarshal([]byte(cm.Data[registriesKey]), &registries)
assert.NoError(t, err)
assert.Equal(t, 0, len(registries))
var secret v1.Secret
err = client.Get(ctx, types.NamespacedName{Name: "addon-registry-test-registry", Namespace: velatypes.DefaultKubeVelaNS}, &secret)
assert.Error(t, err)
assert.True(t, apierrors.IsNotFound(err))
})
}
func TestGetTokenSource(t *testing.T) {
gitSource := &GitAddonSource{URL: "https://github.com/kubevela/catalog.git"}
giteeSource := &GiteeAddonSource{URL: "https://gitee.com/kubevela/catalog.git"}
gitlabSource := &GitlabAddonSource{URL: "https://gitlab.com/kubevela/catalog.git"}
testCases := []struct {
name string
registry *Registry
expectedSource TokenSource
}{
{
name: "git source",
registry: &Registry{
Git: gitSource,
},
expectedSource: gitSource,
},
{
name: "gitee source",
registry: &Registry{
Gitee: giteeSource,
},
expectedSource: giteeSource,
},
{
name: "gitlab source",
registry: &Registry{
Gitlab: gitlabSource,
},
expectedSource: gitlabSource,
},
{
name: "git and gitee source",
registry: &Registry{
Git: gitSource,
Gitee: giteeSource,
},
expectedSource: gitSource,
},
{
name: "gitee and gitlab source",
registry: &Registry{
Gitee: giteeSource,
Gitlab: gitlabSource,
},
expectedSource: giteeSource,
},
{
name: "all token sources",
registry: &Registry{
Git: gitSource,
Gitee: giteeSource,
Gitlab: gitlabSource,
},
expectedSource: gitSource,
},
{
name: "no token source",
registry: &Registry{
Helm: &HelmSource{},
},
expectedSource: nil,
},
{
name: "empty registry",
registry: &Registry{},
expectedSource: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
source := tc.registry.GetTokenSource()
assert.Equal(t, tc.expectedSource, source)
})
}
}
func TestAddRegistry(t *testing.T) {
t.Run("Test adding a registry", func(t *testing.T) {
ctx := context.Background()
testRegistry := Registry{
Name: "test-registry",
Git: &GitAddonSource{
URL: "http://github.com/test/repo",
Token: "test-token",
},
}
scheme := runtime.NewScheme()
assert.NoError(t, v1.AddToScheme(scheme))
client := fake.NewClientBuilder().WithScheme(scheme).Build()
ds := NewRegistryDataStore(client)
err := ds.AddRegistry(ctx, testRegistry)
assert.NoError(t, err)
var cm v1.ConfigMap
err = client.Get(ctx, types.NamespacedName{Name: registryConfigMapName, Namespace: velatypes.DefaultKubeVelaNS}, &cm)
assert.NoError(t, err)
var registries map[string]Registry
err = json.Unmarshal([]byte(cm.Data[registriesKey]), &registries)
assert.NoError(t, err)
assert.Equal(t, 1, len(registries))
gotRegistry := registries["test-registry"]
assert.Equal(t, "", gotRegistry.Git.Token)
assert.Equal(t, "addon-registry-test-registry", gotRegistry.Git.TokenSecretRef)
var secret v1.Secret
err = client.Get(ctx, types.NamespacedName{Name: "addon-registry-test-registry", Namespace: velatypes.DefaultKubeVelaNS}, &secret)
assert.NoError(t, err)
assert.Equal(t, "test-token", string(secret.Data["token"]))
})
}
func TestUpdateRegistry(t *testing.T) {
t.Run("Test updating a registry", func(t *testing.T) {
ctx := context.Background()
updatedRegistry := Registry{
Name: "test-registry",
Git: &GitAddonSource{
URL: "http://github.com/test/repo-updated",
Token: "test-token-updated",
},
}
// Pre-existing state
existingRegistry := Registry{
Name: "test-registry",
Git: &GitAddonSource{
URL: "http://github.com/test/repo",
TokenSecretRef: "addon-registry-test-registry",
},
}
registries := map[string]Registry{"test-registry": existingRegistry}
registriesBytes, err := json.Marshal(registries)
assert.NoError(t, err)
cm := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: registryConfigMapName,
Namespace: velatypes.DefaultKubeVelaNS,
},
Data: map[string]string{
registriesKey: string(registriesBytes),
},
}
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "addon-registry-test-registry",
Namespace: velatypes.DefaultKubeVelaNS,
},
Data: map[string][]byte{
"token": []byte("test-token"),
},
}
scheme := runtime.NewScheme()
assert.NoError(t, v1.AddToScheme(scheme))
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cm, secret).Build()
ds := NewRegistryDataStore(client)
err = ds.UpdateRegistry(ctx, updatedRegistry)
assert.NoError(t, err)
var updatedSecret v1.Secret
err = client.Get(ctx, types.NamespacedName{Name: "addon-registry-test-registry", Namespace: velatypes.DefaultKubeVelaNS}, &updatedSecret)
assert.NoError(t, err)
assert.Equal(t, "test-token-updated", string(updatedSecret.Data["token"]))
var updatedCm v1.ConfigMap
err = client.Get(ctx, types.NamespacedName{Name: registryConfigMapName, Namespace: velatypes.DefaultKubeVelaNS}, &updatedCm)
assert.NoError(t, err)
var updatedRegistries map[string]Registry
err = json.Unmarshal([]byte(updatedCm.Data[registriesKey]), &updatedRegistries)
assert.NoError(t, err)
assert.Equal(t, "http://github.com/test/repo-updated", updatedRegistries["test-registry"].Git.URL)
})
}
func TestListRegistry(t *testing.T) {
t.Run("Test listing registries", func(t *testing.T) {
ctx := context.Background()
// Pre-existing state
existingRegistry := Registry{
Name: "test-registry",
Git: &GitAddonSource{
URL: "http://github.com/test/repo",
TokenSecretRef: "addon-registry-test-registry",
},
}
registries := map[string]Registry{"test-registry": existingRegistry}
registriesBytes, err := json.Marshal(registries)
assert.NoError(t, err)
cm := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: registryConfigMapName,
Namespace: velatypes.DefaultKubeVelaNS,
},
Data: map[string]string{
registriesKey: string(registriesBytes),
},
}
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "addon-registry-test-registry",
Namespace: velatypes.DefaultKubeVelaNS,
},
Data: map[string][]byte{
"token": []byte("test-token"),
},
}
scheme := runtime.NewScheme()
assert.NoError(t, v1.AddToScheme(scheme))
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cm, secret).Build()
ds := NewRegistryDataStore(client)
// Test List
listedRegistries, err := ds.ListRegistries(ctx)
assert.NoError(t, err)
assert.Equal(t, 1, len(listedRegistries))
assert.Equal(t, "test-token", listedRegistries[0].Git.Token)
assert.Equal(t, "http://github.com/test/repo", listedRegistries[0].Git.URL)
})
}
func TestGetRegistry(t *testing.T) {
t.Run("Test getting a single registry", func(t *testing.T) {
ctx := context.Background()
// Pre-existing state
existingRegistry := Registry{
Name: "test-registry",
Git: &GitAddonSource{
URL: "http://github.com/test/repo",
TokenSecretRef: "addon-registry-test-registry",
},
}
registries := map[string]Registry{"test-registry": existingRegistry}
registriesBytes, err := json.Marshal(registries)
assert.NoError(t, err)
cm := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: registryConfigMapName,
Namespace: velatypes.DefaultKubeVelaNS,
},
Data: map[string]string{
registriesKey: string(registriesBytes),
},
}
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "addon-registry-test-registry",
Namespace: velatypes.DefaultKubeVelaNS,
},
Data: map[string][]byte{
"token": []byte("test-token"),
},
}
scheme := runtime.NewScheme()
assert.NoError(t, v1.AddToScheme(scheme))
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cm, secret).Build()
ds := NewRegistryDataStore(client)
// Test Get
reg, err := ds.GetRegistry(ctx, "test-registry")
assert.NoError(t, err)
assert.Equal(t, "test-token", reg.Git.Token)
assert.Equal(t, "http://github.com/test/repo", reg.Git.URL)
})
}
func TestDeleteRegistry(t *testing.T) {
t.Run("Test deleting a registry", func(t *testing.T) {
ctx := context.Background()
// Pre-existing state
existingRegistry := Registry{
Name: "test-registry",
Git: &GitAddonSource{
URL: "http://github.com/test/repo",
TokenSecretRef: "addon-registry-test-registry",
},
}
registries := map[string]Registry{"test-registry": existingRegistry}
registriesBytes, err := json.Marshal(registries)
assert.NoError(t, err)
cm := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: registryConfigMapName,
Namespace: velatypes.DefaultKubeVelaNS,
},
Data: map[string]string{
registriesKey: string(registriesBytes),
},
}
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "addon-registry-test-registry",
Namespace: velatypes.DefaultKubeVelaNS,
},
Data: map[string][]byte{
"token": []byte("test-token"),
},
}
scheme := runtime.NewScheme()
assert.NoError(t, v1.AddToScheme(scheme))
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cm, secret).Build()
ds := NewRegistryDataStore(client)
err = ds.DeleteRegistry(ctx, "test-registry")
assert.NoError(t, err)
var updatedCm v1.ConfigMap
err = client.Get(ctx, types.NamespacedName{Name: registryConfigMapName, Namespace: velatypes.DefaultKubeVelaNS}, &updatedCm)
assert.NoError(t, err)
var updatedRegistries map[string]Registry
err = json.Unmarshal([]byte(updatedCm.Data[registriesKey]), &updatedRegistries)
assert.NoError(t, err)
assert.Equal(t, 0, len(updatedRegistries))
var deletedSecret v1.Secret
err = client.Get(ctx, types.NamespacedName{Name: "addon-registry-test-registry", Namespace: velatypes.DefaultKubeVelaNS}, &deletedSecret)
assert.Error(t, err)
assert.True(t, apierrors.IsNotFound(err))
})
}
func TestGetRegistries(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()
assert.NoError(t, v1.AddToScheme(scheme))
// valid configmap with one registry
validRegistries := map[string]Registry{"test-registry": {Name: "test-registry"}}
validRegistriesBytes, err := json.Marshal(validRegistries)
assert.NoError(t, err)
validCm := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: registryConfigMapName, Namespace: velatypes.DefaultKubeVelaNS},
Data: map[string]string{registriesKey: string(validRegistriesBytes)},
}
// configmap with invalid json
invalidJSONCm := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: registryConfigMapName, Namespace: velatypes.DefaultKubeVelaNS},
Data: map[string]string{registriesKey: "invalid-json"},
}
// configmap with missing key
missingKeyCm := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: registryConfigMapName, Namespace: velatypes.DefaultKubeVelaNS},
Data: map[string]string{"another-key": "some-data"},
}
testCases := map[string]struct {
client client.Client
expectErr bool
expectRegNum int
}{
"success": {
client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(validCm).Build(),
expectErr: false,
expectRegNum: 1,
},
"configmap not found": {
client: fake.NewClientBuilder().WithScheme(scheme).Build(),
expectErr: true,
},
"invalid json": {
client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(invalidJSONCm).Build(),
expectErr: true,
},
"registries key missing": {
client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(missingKeyCm).Build(),
expectErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
ds := registryImpl{client: tc.client}
registries, _, err := ds.getRegistries(ctx)
if tc.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectRegNum, len(registries))
}
})
}
}
func TestLoadTokenFromSecret(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()
assert.NoError(t, v1.AddToScheme(scheme))
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "addon-registry-test", Namespace: velatypes.DefaultKubeVelaNS},
Data: map[string][]byte{"token": []byte("test-token")},
}
testCases := map[string]struct {
client client.Client
registry *Registry
expectErr bool
expectToken string
}{
"success": {
client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build(),
registry: &Registry{
Name: "test",
Git: &GitAddonSource{URL: "http://github.com/test/repo", TokenSecretRef: "addon-registry-test"},
},
expectErr: false,
expectToken: "test-token",
},
"secret not found": {
client: fake.NewClientBuilder().WithScheme(scheme).Build(),
registry: &Registry{
Name: "test",
Git: &GitAddonSource{URL: "http://github.com/test/repo", TokenSecretRef: "addon-registry-test"},
},
expectErr: false,
expectToken: "",
},
"no token source": {
client: fake.NewClientBuilder().WithScheme(scheme).Build(),
registry: &Registry{Name: "test"},
expectErr: false,
expectToken: "",
},
"no secret ref": {
client: fake.NewClientBuilder().WithScheme(scheme).Build(),
registry: &Registry{Name: "test", Git: &GitAddonSource{URL: "http://github.com/test/repo"}},
expectErr: false,
expectToken: "",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
err := loadTokenFromSecret(ctx, tc.client, tc.registry)
if tc.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
if tc.registry.Git != nil {
assert.Equal(t, tc.expectToken, tc.registry.Git.Token)
}
}
})
}
}
func TestCreateOrUpdateTokenSecret(t *testing.T) {
ctx := context.Background()
scheme := runtime.NewScheme()
assert.NoError(t, v1.AddToScheme(scheme))
existingSecret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "addon-registry-test", Namespace: velatypes.DefaultKubeVelaNS},
Data: map[string][]byte{"token": []byte("old-token")},
}
testCases := map[string]struct {
client client.Client
registry *Registry
expectErr bool
expectToken string
expectSecret bool
}{
"create new secret": {
client: fake.NewClientBuilder().WithScheme(scheme).Build(),
registry: &Registry{
Name: "test",
Git: &GitAddonSource{Token: "new-token"},
},
expectErr: false,
expectToken: "new-token",
expectSecret: true,
},
"update existing secret": {
client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(existingSecret).Build(),
registry: &Registry{
Name: "test",
Git: &GitAddonSource{Token: "updated-token"},
},
expectErr: false,
expectToken: "updated-token",
expectSecret: true,
},
"no token source": {
client: fake.NewClientBuilder().WithScheme(scheme).Build(),
registry: &Registry{Name: "test"},
expectErr: false,
expectSecret: false,
},
"empty token": {
client: fake.NewClientBuilder().WithScheme(scheme).Build(),
registry: &Registry{Name: "test", Git: &GitAddonSource{Token: ""}},
expectErr: false,
expectSecret: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
err := createOrUpdateTokenSecret(ctx, tc.client, tc.registry)
if tc.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
if tc.expectSecret {
var secret v1.Secret
err := tc.client.Get(ctx, types.NamespacedName{Name: "addon-registry-test", Namespace: velatypes.DefaultKubeVelaNS}, &secret)
assert.NoError(t, err)
assert.Equal(t, tc.expectToken, string(secret.Data["token"]))
assert.Equal(t, "addon-registry-test", tc.registry.GetTokenSource().GetTokenSecretRef())
} else {
var secret v1.Secret
err := tc.client.Get(ctx, types.NamespacedName{Name: "addon-registry-test", Namespace: velatypes.DefaultKubeVelaNS}, &secret)
assert.True(t, apierrors.IsNotFound(err))
}
}
})
}
}

View File

@ -56,37 +56,32 @@ type Source interface {
// GitAddonSource defines the information about the Git as addon source
type GitAddonSource struct {
URL string `json:"url,omitempty" validate:"required"`
Path string `json:"path,omitempty"`
Token string `json:"token,omitempty"`
URL string `json:"url,omitempty" validate:"required"`
Path string `json:"path,omitempty"`
Token string `json:"token,omitempty"`
TokenSecretRef string `json:"tokenSecretRef,omitempty"`
}
// GiteeAddonSource defines the information about the Gitee as addon source
type GiteeAddonSource struct {
URL string `json:"url,omitempty" validate:"required"`
Path string `json:"path,omitempty"`
Token string `json:"token,omitempty"`
// GetToken returns the token of the source
func (g *GitAddonSource) GetToken() string {
return g.Token
}
// GitlabAddonSource defines the information about the Gitlab as addon source
type GitlabAddonSource struct {
URL string `json:"url,omitempty" validate:"required"`
Repo string `json:"repo,omitempty" validate:"required"`
Path string `json:"path,omitempty"`
Token string `json:"token,omitempty"`
// SetToken set the token of the source
func (g *GitAddonSource) SetToken(token string) {
g.Token = token
g.TokenSecretRef = ""
}
// HelmSource defines the information about the helm repo addon source
type HelmSource struct {
URL string `json:"url,omitempty" validate:"required"`
InsecureSkipTLS bool `json:"insecureSkipTLS,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
// SetTokenSecretRef set the token secret ref to the source
func (g *GitAddonSource) SetTokenSecretRef(secretName string) {
g.Token = ""
g.TokenSecretRef = secretName
}
// SafeCopier is an interface to copy Struct without sensitive fields, such as Token, Username, Password
type SafeCopier interface {
SafeCopy() interface{}
// GetTokenSecretRef return the token secret ref of the source
func (g *GitAddonSource) GetTokenSecretRef() string {
return g.TokenSecretRef
}
// SafeCopy hides field Token
@ -95,22 +90,85 @@ func (g *GitAddonSource) SafeCopy() *GitAddonSource {
return nil
}
return &GitAddonSource{
URL: g.URL,
Path: g.Path,
URL: g.URL,
Path: g.Path,
TokenSecretRef: g.TokenSecretRef,
}
}
// GiteeAddonSource defines the information about the Gitee as addon source
type GiteeAddonSource struct {
URL string `json:"url,omitempty" validate:"required"`
Path string `json:"path,omitempty"`
Token string `json:"token,omitempty"`
TokenSecretRef string `json:"tokenSecretRef,omitempty"`
}
// GetToken return the token of the source
func (g *GiteeAddonSource) GetToken() string {
return g.Token
}
// SetToken set the token of the source
func (g *GiteeAddonSource) SetToken(token string) {
g.Token = token
g.TokenSecretRef = ""
}
// SetTokenSecretRef set the token secret ref to the source
func (g *GiteeAddonSource) SetTokenSecretRef(secretName string) {
g.Token = ""
g.TokenSecretRef = secretName
}
// GetTokenSecretRef return the token secret ref of the source
func (g *GiteeAddonSource) GetTokenSecretRef() string {
return g.TokenSecretRef
}
// SafeCopy hides field Token
func (g *GiteeAddonSource) SafeCopy() *GiteeAddonSource {
if g == nil {
return nil
}
return &GiteeAddonSource{
URL: g.URL,
Path: g.Path,
URL: g.URL,
Path: g.Path,
TokenSecretRef: g.TokenSecretRef,
}
}
// GitlabAddonSource defines the information about Gitlab as an addon source
type GitlabAddonSource struct {
URL string `json:"url,omitempty" validate:"required"`
Repo string `json:"repo,omitempty" validate:"required"`
Path string `json:"path,omitempty"`
Token string `json:"token,omitempty"`
TokenSecretRef string `json:"tokenSecretRef,omitempty"`
}
// GetToken return the token of the source
func (g *GitlabAddonSource) GetToken() string {
return g.Token
}
// SetToken set the token of the source
func (g *GitlabAddonSource) SetToken(token string) {
g.Token = token
g.TokenSecretRef = ""
}
// SetTokenSecretRef set the token secret ref to the source
func (g *GitlabAddonSource) SetTokenSecretRef(secretName string) {
g.Token = ""
g.TokenSecretRef = secretName
}
// GetTokenSecretRef return the token secret ref of the source
func (g *GitlabAddonSource) GetTokenSecretRef() string {
return g.TokenSecretRef
}
// SafeCopy hides field Token
func (g *GitlabAddonSource) SafeCopy() *GitlabAddonSource {
if g == nil {
@ -123,6 +181,19 @@ func (g *GitlabAddonSource) SafeCopy() *GitlabAddonSource {
}
}
// HelmSource defines the information about the helm repo addon source
type HelmSource struct {
URL string `json:"url,omitempty" validate:"required"`
InsecureSkipTLS bool `json:"insecureSkipTLS,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
}
// SafeCopier is an interface to copy struct without sensitive fields, such as Token, Username, Password
type SafeCopier interface {
SafeCopy() interface{}
}
// SafeCopy hides field Username, Password
func (h *HelmSource) SafeCopy() *HelmSource {
if h == nil {

View File

@ -275,3 +275,47 @@ func TestSafeCopy(t *testing.T) {
assert.Empty(t, shelm.Password)
assert.Equal(t, "https://hub.vela.com/chartrepo/addons", shelm.URL)
}
func TestTokenSource(t *testing.T) {
t.Run("GitAddonSource", func(t *testing.T) {
source := &GitAddonSource{}
assert.Equal(t, "", source.GetToken())
assert.Equal(t, "", source.GetTokenSecretRef())
source.SetToken("test-token")
assert.Equal(t, "test-token", source.GetToken())
assert.Equal(t, "", source.GetTokenSecretRef())
source.SetTokenSecretRef("test-secret")
assert.Equal(t, "test-secret", source.GetTokenSecretRef())
assert.Equal(t, "", source.GetToken())
})
t.Run("GiteeAddonSource", func(t *testing.T) {
source := &GiteeAddonSource{}
assert.Equal(t, "", source.GetToken())
assert.Equal(t, "", source.GetTokenSecretRef())
source.SetToken("test-token")
assert.Equal(t, "test-token", source.GetToken())
assert.Equal(t, "", source.GetTokenSecretRef())
source.SetTokenSecretRef("test-secret")
assert.Equal(t, "test-secret", source.GetTokenSecretRef())
assert.Equal(t, "", source.GetToken())
})
t.Run("GitlabAddonSource", func(t *testing.T) {
source := &GitlabAddonSource{}
assert.Equal(t, "", source.GetToken())
assert.Equal(t, "", source.GetTokenSecretRef())
source.SetToken("test-token")
assert.Equal(t, "test-token", source.GetToken())
assert.Equal(t, "", source.GetTokenSecretRef())
source.SetTokenSecretRef("test-secret")
assert.Equal(t, "test-secret", source.GetTokenSecretRef())
assert.Equal(t, "", source.GetToken())
})
}

View File

@ -465,7 +465,12 @@ func checkConflictDefs(ctx context.Context, k8sClient client.Client, defs []*uns
}
if owner.Name != appName {
// if addon not belong to an addon or addon name is another one, we should put them in result
res[checkDef.GetName()] = fmt.Sprintf("definition: %s in this addon already exist in %s \n", checkDef.GetName(), addon.AppName2Addon(appName))
addonName := addon.AppName2Addon(owner.Name)
// If owner.Name isn't an addon app name, show the owner's name directly as the addon name
if addonName == "" {
addonName = owner.Name
}
res[checkDef.GetName()] = fmt.Sprintf("definition: %s in this addon already exist in %s \n", checkDef.GetName(), addonName)
}
}
if err != nil && !errors2.IsNotFound(err) {

View File

@ -117,6 +117,86 @@ var _ = Describe("Test definition check", func() {
Expect(err).Should(BeNil())
Expect(len(usedApps)).Should(BeEquivalentTo(4))
})
It("Test checkConflictDefs", func() {
const appName = "addon-fluxcd"
const otherAppName = "addon-other"
isController := true
// A definition that doesn't exist in the cluster
nonExistingDef := &unstructured.Unstructured{}
nonExistingDef.SetAPIVersion("core.oam.dev/v1beta1")
nonExistingDef.SetKind("ComponentDefinition")
nonExistingDef.SetName("non-existing-def")
nonExistingDef.SetNamespace(velatypes.DefaultKubeVelaNS)
// A definition that exists but has no owner
defWithNoOwner := &unstructured.Unstructured{}
defWithNoOwner.SetAPIVersion("core.oam.dev/v1beta1")
defWithNoOwner.SetKind("ComponentDefinition")
defWithNoOwner.SetName("def-no-owner")
defWithNoOwner.SetNamespace(velatypes.DefaultKubeVelaNS)
Expect(k8sClient.Create(ctx, defWithNoOwner)).Should(Succeed())
defer func() {
Expect(k8sClient.Delete(ctx, defWithNoOwner)).Should(Succeed())
}()
// A definition that is owned by another addon
defWithOtherOwner := &unstructured.Unstructured{}
defWithOtherOwner.SetAPIVersion("core.oam.dev/v1beta1")
defWithOtherOwner.SetKind("ComponentDefinition")
defWithOtherOwner.SetName("def-other-owner")
defWithOtherOwner.SetNamespace(velatypes.DefaultKubeVelaNS)
defWithOtherOwner.SetOwnerReferences([]metav1.OwnerReference{
{
APIVersion: v1beta1.SchemeGroupVersion.String(),
Kind: v1beta1.ApplicationKind,
Name: otherAppName,
Controller: &isController,
UID: "test-uid-other",
},
})
Expect(k8sClient.Create(ctx, defWithOtherOwner)).Should(Succeed())
defer func() {
Expect(k8sClient.Delete(ctx, defWithOtherOwner)).Should(Succeed())
}()
// A definition that is owned by the same addon
defWithSameOwner := &unstructured.Unstructured{}
defWithSameOwner.SetAPIVersion("core.oam.dev/v1beta1")
defWithSameOwner.SetKind("ComponentDefinition")
defWithSameOwner.SetName("def-same-owner")
defWithSameOwner.SetNamespace(velatypes.DefaultKubeVelaNS)
defWithSameOwner.SetOwnerReferences([]metav1.OwnerReference{
{
APIVersion: v1beta1.SchemeGroupVersion.String(),
Kind: v1beta1.ApplicationKind,
Name: appName,
Controller: &isController,
UID: "test-uid-same",
},
})
Expect(k8sClient.Create(ctx, defWithSameOwner)).Should(Succeed())
defer func() {
Expect(k8sClient.Delete(ctx, defWithSameOwner)).Should(Succeed())
}()
By("Checking a mix of definitions for conflicts")
defs := []*unstructured.Unstructured{
nonExistingDef,
defWithNoOwner,
defWithOtherOwner,
defWithSameOwner,
}
conflicts, err := checkConflictDefs(ctx, k8sClient, defs, appName)
Expect(err).Should(BeNil())
Expect(len(conflicts)).Should(Equal(2))
Expect(conflicts).Should(HaveKey(defWithNoOwner.GetName()))
Expect(conflicts[defWithNoOwner.GetName()]).Should(ContainSubstring("already exist and not belong to any addon"))
Expect(conflicts).Should(HaveKey(defWithOtherOwner.GetName()))
Expect(conflicts[defWithOtherOwner.GetName()]).Should(ContainSubstring("already exist in other"))
})
})
func TestMerge2Map(t *testing.T) {

View File

@ -0,0 +1,261 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package watcher
import (
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
)
func TestApplicationMetricsWatcher(t *testing.T) {
t.Parallel()
appRunning := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{Name: "app-running"},
Status: common.AppStatus{
Phase: common.ApplicationRunning,
},
}
appRendering := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{Name: "app-rendering"},
Status: common.AppStatus{
Phase: common.ApplicationRendering,
},
}
appWithWorkflow := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{Name: "app-with-workflow"},
Status: common.AppStatus{
Phase: common.ApplicationRunning,
Workflow: &common.WorkflowStatus{
Steps: []workflowv1alpha1.WorkflowStepStatus{
{
StepStatus: workflowv1alpha1.StepStatus{
Name: "step1",
Type: "apply-component",
Phase: workflowv1alpha1.WorkflowStepPhaseSucceeded,
},
},
},
},
},
}
appWithMixedWorkflow := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{Name: "app-with-mixed-workflow"},
Status: common.AppStatus{
Phase: common.ApplicationRunning,
Workflow: &common.WorkflowStatus{
Steps: []workflowv1alpha1.WorkflowStepStatus{
{
StepStatus: workflowv1alpha1.StepStatus{
Name: "step1",
Type: "apply-component",
Phase: workflowv1alpha1.WorkflowStepPhaseSucceeded,
},
},
{
StepStatus: workflowv1alpha1.StepStatus{
Name: "step2",
Type: "apply-component",
Phase: workflowv1alpha1.WorkflowStepPhaseFailed,
},
},
{
StepStatus: workflowv1alpha1.StepStatus{
Name: "step3",
Type: "suspend",
Phase: workflowv1alpha1.WorkflowStepPhaseRunning,
},
},
},
},
},
}
testCases := map[string]struct {
app *v1beta1.Application
op int
wantPC map[string]int
wantSC map[string]int
wantPD map[string]struct{}
wantSD map[string]struct{}
}{
"Add an application": {
app: appRunning,
op: 1,
wantPC: map[string]int{"running": 1},
wantSC: map[string]int{},
wantPD: map[string]struct{}{"running": {}},
wantSD: map[string]struct{}{},
},
"Add an application with workflow": {
app: appWithWorkflow,
op: 1,
wantPC: map[string]int{"running": 1},
wantSC: map[string]int{"apply-component/succeeded#": 1},
wantPD: map[string]struct{}{"running": {}},
wantSD: map[string]struct{}{"apply-component/succeeded#": {}},
},
"Delete an application": {
app: appRunning,
op: -1,
wantPC: map[string]int{"running": -1},
wantSC: map[string]int{},
wantPD: map[string]struct{}{"running": {}},
wantSD: map[string]struct{}{},
},
"Update an application": {
app: appRendering,
op: -1,
wantPC: map[string]int{"rendering": -1},
wantSC: map[string]int{},
wantPD: map[string]struct{}{"rendering": {}},
wantSD: map[string]struct{}{},
},
"Nil app status": {
app: &v1beta1.Application{},
op: 1,
wantPC: map[string]int{"-": 1},
wantSC: map[string]int{},
wantPD: map[string]struct{}{"-": {}},
wantSD: map[string]struct{}{},
},
"Add an application with mixed workflow": {
app: appWithMixedWorkflow,
op: 1,
wantPC: map[string]int{"running": 1},
wantSC: map[string]int{
"apply-component/succeeded#": 1,
"apply-component/failed#": 1,
"suspend/running#": 1,
},
wantPD: map[string]struct{}{"running": {}},
wantSD: map[string]struct{}{
"apply-component/succeeded#": {},
"apply-component/failed#": {},
"suspend/running#": {},
},
},
"Empty workflow steps": {
app: &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{Name: "app-empty-workflow"},
Status: common.AppStatus{
Phase: common.ApplicationRunning,
Workflow: &common.WorkflowStatus{
Steps: []workflowv1alpha1.WorkflowStepStatus{},
},
},
},
op: 1,
wantPC: map[string]int{"running": 1},
wantSC: map[string]int{},
wantPD: map[string]struct{}{"running": {}},
wantSD: map[string]struct{}{},
},
"Unknown phase": {
app: &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{Name: "app-unknown-phase"},
Status: common.AppStatus{
Phase: "unknown",
},
},
op: 1,
wantPC: map[string]int{"unknown": 1},
wantSC: map[string]int{},
wantPD: map[string]struct{}{"unknown": {}},
wantSD: map[string]struct{}{},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
watcher := &applicationMetricsWatcher{
phaseCounter: map[string]int{},
stepPhaseCounter: map[string]int{},
phaseDirty: map[string]struct{}{},
stepPhaseDirty: map[string]struct{}{},
}
watcher.inc(tc.app, tc.op)
assert.Equal(t, tc.wantPC, watcher.phaseCounter)
assert.Equal(t, tc.wantSC, watcher.stepPhaseCounter)
assert.Equal(t, tc.wantPD, watcher.phaseDirty)
assert.Equal(t, tc.wantSD, watcher.stepPhaseDirty)
})
}
t.Run("Idempotence", func(t *testing.T) {
t.Parallel()
watcher := &applicationMetricsWatcher{
phaseCounter: map[string]int{},
stepPhaseCounter: map[string]int{},
phaseDirty: map[string]struct{}{},
stepPhaseDirty: map[string]struct{}{},
}
watcher.inc(appRunning, 1)
watcher.inc(appRunning, 1)
assert.Equal(t, map[string]int{"running": 2}, watcher.phaseCounter)
assert.Equal(t, map[string]struct{}{"running": {}}, watcher.phaseDirty)
})
t.Run("Report should clear dirty flags", func(t *testing.T) {
t.Parallel()
watcher := &applicationMetricsWatcher{
phaseCounter: map[string]int{"running": 1},
stepPhaseCounter: map[string]int{"apply-component/succeeded#": 1},
phaseDirty: map[string]struct{}{"running": {}},
stepPhaseDirty: map[string]struct{}{"apply-component/succeeded#": {}},
}
watcher.report()
assert.Empty(t, watcher.phaseDirty)
assert.Empty(t, watcher.stepPhaseDirty)
assert.Equal(t, map[string]int{"running": 1}, watcher.phaseCounter)
assert.Equal(t, map[string]int{"apply-component/succeeded#": 1}, watcher.stepPhaseCounter)
})
t.Run("getPhase helper function", func(t *testing.T) {
t.Parallel()
watcher := &applicationMetricsWatcher{}
assert.Equal(t, "-", watcher.getPhase(""))
assert.Equal(t, "running", watcher.getPhase("running"))
})
t.Run("getApp helper function", func(t *testing.T) {
t.Parallel()
watcher := &applicationMetricsWatcher{}
inputApp := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "test-ns",
},
Status: common.AppStatus{
Phase: common.ApplicationRunning,
},
}
resultApp := watcher.getApp(inputApp)
assert.NotNil(t, resultApp)
assert.Equal(t, "test-app", resultApp.Name)
assert.Equal(t, "test-ns", resultApp.Namespace)
assert.Equal(t, common.ApplicationRunning, resultApp.Status.Phase)
})
}

View File

@ -17,13 +17,19 @@
package velaql
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseVelaQL(t *testing.T) {
t.Parallel()
testcases := []struct {
ql string
query QueryView
@ -67,19 +73,22 @@ func TestParseVelaQL(t *testing.T) {
err: nil,
}}
for _, testcase := range testcases {
q, err := ParseVelaQL(testcase.ql)
assert.Equal(t, testcase.err != nil, err != nil)
if err == nil {
assert.Equal(t, testcase.query.View, q.View)
assert.Equal(t, testcase.query.Export, q.Export)
} else {
assert.Equal(t, testcase.err.Error(), err.Error())
}
for i, testcase := range testcases {
t.Run(fmt.Sprintf("testcase-%d", i), func(t *testing.T) {
q, err := ParseVelaQL(testcase.ql)
if testcase.err != nil {
assert.EqualError(t, testcase.err, err.Error())
} else {
assert.NoError(t, err)
assert.Equal(t, testcase.query.View, q.View)
assert.Equal(t, testcase.query.Export, q.Export)
}
})
}
}
func TestParseParameter(t *testing.T) {
t.Parallel()
testcases := []struct {
parameter string
parameterMap map[string]interface{}
@ -122,15 +131,114 @@ func TestParseParameter(t *testing.T) {
err: nil,
}}
for _, testcase := range testcases {
result, err := ParseParameter(testcase.parameter)
assert.Equal(t, testcase.err != nil, err != nil)
if err == nil {
for k, v := range result {
assert.Equal(t, testcase.parameterMap[k], v)
for i, testcase := range testcases {
t.Run(fmt.Sprintf("testcase-%d", i), func(t *testing.T) {
result, err := ParseParameter(testcase.parameter)
if testcase.err != nil {
assert.EqualError(t, testcase.err, err.Error())
} else {
assert.NoError(t, err)
for k, v := range result {
assert.Equal(t, testcase.parameterMap[k], v)
}
}
} else {
assert.Equal(t, testcase.err.Error(), err.Error())
}
})
}
}
func TestParseVelaQLFromPath(t *testing.T) {
t.Parallel()
ctx := context.Background()
testdataDir := "testdata"
testcases := []struct {
name string
path string
expectedExport string
expectError bool
errorContains string
}{
{
name: "Simple valid CUE file with export field",
path: filepath.Join(testdataDir, "simple-valid.cue"),
expectedExport: "output.message",
expectError: false,
},
{
name: "Simple valid CUE file without export field",
path: filepath.Join(testdataDir, "simple-no-export.cue"),
expectedExport: DefaultExportValue,
expectError: false,
},
{
name: "Nonexistent file path",
path: filepath.Join(testdataDir, "nonexistent.cue"),
expectError: true,
errorContains: "read view file from",
},
{
name: "Empty file path",
path: "",
expectError: true,
errorContains: "read view file from",
},
{
name: "Invalid CUE content",
path: filepath.Join(testdataDir, "invalid-cue-content.cue"),
expectError: true,
errorContains: "error when parsing view",
},
{
name: "File with invalid export type - should fallback to default",
path: filepath.Join(testdataDir, "invalid-export.cue"),
expectedExport: DefaultExportValue,
expectError: false,
},
{
name: "Empty CUE file",
path: filepath.Join(testdataDir, "empty.cue"),
expectedExport: DefaultExportValue,
expectError: false,
},
{
name: "File with leading/trailing whitespace",
path: filepath.Join(testdataDir, "whitespace.cue"),
expectedExport: "output.message",
expectError: false,
},
{
name: "Relative path",
path: "testdata/nonexistent.cue",
expectError: true,
errorContains: "read view file from",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
result, err := ParseVelaQLFromPath(ctx, tc.path)
if tc.expectError {
assert.Error(t, err)
assert.Nil(t, result)
if tc.errorContains != "" {
assert.Contains(t, err.Error(), tc.errorContains)
}
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
if tc.path != "" {
expectedContent, readErr := os.ReadFile(tc.path)
require.NoError(t, readErr)
assert.Equal(t, string(expectedContent), result.View)
}
assert.Equal(t, tc.expectedExport, result.Export)
assert.Nil(t, result.Parameter)
}
})
}
}

0
pkg/velaql/testdata/empty.cue vendored Normal file
View File

View File

@ -0,0 +1,4 @@
invalid cue syntax [
missing: colon
invalid: brackets
}

View File

@ -0,0 +1,9 @@
parameter: {
name: "test"
}
output: {
message: "hello " + parameter.name
}
export: 123 // Invalid export type (should be string)

View File

@ -0,0 +1,7 @@
parameter: {
name: "test"
}
output: {
message: "hello " + parameter.name
}

9
pkg/velaql/testdata/simple-valid.cue vendored Normal file
View File

@ -0,0 +1,9 @@
parameter: {
name: "test"
}
output: {
message: "hello " + parameter.name
}
export: "output.message"

12
pkg/velaql/testdata/whitespace.cue vendored Normal file
View File

@ -0,0 +1,12 @@
parameter: {
name: "test"
}
output: {
message: "hello " + parameter.name
}
export: "output.message"

View File

@ -29,12 +29,132 @@ import (
dynamicfake "k8s.io/client-go/dynamic/fake"
"cuelang.org/go/cue/errors"
"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1beta1/core"
)
func TestValidateDefinitionRevision(t *testing.T) {
t.Parallel()
scheme := runtime.NewScheme()
v1beta1.AddToScheme(scheme)
baseCompDef := &v1beta1.ComponentDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "test-def",
Namespace: "default",
},
Spec: v1beta1.ComponentDefinitionSpec{
Workload: common.WorkloadTypeDescriptor{
Definition: common.WorkloadGVK{
APIVersion: "apps/v1",
Kind: "Deployment",
},
},
Schematic: &common.Schematic{
CUE: &common.CUE{
Template: `
output: {
apiVersion: "apps/v1"
kind: "Deployment"
metadata: name: context.name
}`,
},
},
},
}
expectedDefRev, _, err := core.GatherRevisionInfo(baseCompDef)
assert.NoError(t, err, "Setup: failed to gather revision info")
expectedDefRev.Name = "test-def-v1"
expectedDefRev.Namespace = "default"
mismatchedHashDefRev := expectedDefRev.DeepCopy()
mismatchedHashDefRev.Spec.RevisionHash = "different-hash"
mismatchedSpecDefRev := expectedDefRev.DeepCopy()
mismatchedSpecDefRev.Spec.ComponentDefinition.Spec.Workload.Definition.Kind = "StatefulSet"
// tweakedCompDef := baseCompDef.DeepCopy()
// tweakedCompDef.Spec.Schematic.CUE.Template = `
// output: {
// apiVersion: "apps/v1"
// kind: "Deployment"
// metadata: name: context.name
// // a tweak
// }`
testCases := map[string]struct {
def runtime.Object
defRevName types.NamespacedName
existingObjs []runtime.Object
expectErr bool
expectedErrContains string
}{
"Success with matching definition revision": {
def: baseCompDef,
defRevName: types.NamespacedName{Name: "test-def-v1", Namespace: "default"},
existingObjs: []runtime.Object{expectedDefRev},
expectErr: false,
},
"Success when definition revision does not exist": {
def: baseCompDef,
defRevName: types.NamespacedName{Name: "test-def-v1", Namespace: "default"},
existingObjs: []runtime.Object{},
expectErr: false,
},
"Failure with revision hash mismatch": {
def: baseCompDef,
defRevName: types.NamespacedName{Name: "test-def-v1", Namespace: "default"},
existingObjs: []runtime.Object{mismatchedHashDefRev},
expectErr: true,
expectedErrContains: "the definition's spec is different with existing definitionRevision's spec",
},
"Failure with spec mismatch (DeepEqual)": {
def: baseCompDef,
defRevName: types.NamespacedName{Name: "test-def-v1", Namespace: "default"},
existingObjs: []runtime.Object{mismatchedSpecDefRev},
expectErr: true,
expectedErrContains: "the definition's spec is different with existing definitionRevision's spec",
},
"Failure with invalid definition revision name": {
def: baseCompDef,
defRevName: types.NamespacedName{Name: "invalid!name", Namespace: "default"},
existingObjs: []runtime.Object{},
expectErr: true,
expectedErrContains: "invalid definitionRevision name",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
cli := fake.NewClientBuilder().
WithScheme(scheme).
WithRuntimeObjects(tc.existingObjs...).
Build()
err := ValidateDefinitionRevision(context.Background(), cli, tc.def, tc.defRevName)
if tc.expectErr {
assert.Error(t, err)
if tc.expectedErrContains != "" {
assert.Contains(t, err.Error(), tc.expectedErrContains)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidateCueTemplate(t *testing.T) {
t.Parallel()
cases := map[string]struct {
cueTemplate string
want error
@ -66,19 +186,31 @@ func TestValidateCueTemplate(t *testing.T) {
}`,
want: errors.New("output.hello: reference \"world\" not found"),
},
"emptyCueTemp": {
cueTemplate: "",
want: nil,
},
"malformedCueTemp": {
cueTemplate: "output: { metadata: { name: context.name, label: context.label, annotation: \"default\" }, hello: world ",
want: errors.New("expected '}', found 'EOF'"),
},
}
for caseName, cs := range cases {
t.Run(caseName, func(t *testing.T) {
t.Parallel()
err := ValidateCueTemplate(cs.cueTemplate)
if diff := cmp.Diff(cs.want, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nValidateCueTemplate: -want , +got \n%s\n", cs.want, diff)
if cs.want != nil {
assert.EqualError(t, cs.want, err.Error())
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidateCuexTemplate(t *testing.T) {
t.Parallel()
cases := map[string]struct {
cueTemplate string
want error
@ -164,15 +296,19 @@ func TestValidateCuexTemplate(t *testing.T) {
for caseName, cs := range cases {
t.Run(caseName, func(t *testing.T) {
t.Parallel()
err := ValidateCuexTemplate(context.Background(), cs.cueTemplate)
if diff := cmp.Diff(cs.want, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nValidateCueTemplate: -want , +got \n%s\n", cs.want, diff)
if cs.want != nil {
assert.Equal(t, cs.want.Error(), err.Error())
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidateSemanticVersion(t *testing.T) {
t.Parallel()
cases := map[string]struct {
version string
want error
@ -192,18 +328,20 @@ func TestValidateSemanticVersion(t *testing.T) {
}
for caseName, cs := range cases {
t.Run(caseName, func(t *testing.T) {
t.Parallel()
err := ValidateSemanticVersion(cs.version)
if cs.want != nil {
assert.Equal(t, err.Error(), cs.want.Error())
assert.Error(t, err)
assert.EqualError(t, cs.want, err.Error())
} else {
assert.Equal(t, err, cs.want)
assert.NoError(t, err)
}
})
}
}
func TestValidateMultipleDefVersionsNotPresent(t *testing.T) {
t.Parallel()
cases := map[string]struct {
version string
revisionName string
@ -227,11 +365,13 @@ func TestValidateMultipleDefVersionsNotPresent(t *testing.T) {
}
for caseName, cs := range cases {
t.Run(caseName, func(t *testing.T) {
t.Parallel()
err := ValidateMultipleDefVersionsNotPresent(cs.version, cs.revisionName, "ComponentDefinition")
if cs.want != nil {
assert.Equal(t, err.Error(), cs.want.Error())
assert.Error(t, err)
assert.EqualError(t, cs.want, err.Error())
} else {
assert.Equal(t, err, cs.want)
assert.NoError(t, err)
}
})

View File

@ -48,6 +48,7 @@ func setupClient(ctx context.Context, t *testing.T) client.Client {
}
func TestParser(t *testing.T) {
t.Parallel()
r := require.New(t)
ctx := context.Background()
act := &mock.Action{}
@ -106,7 +107,67 @@ func TestParser(t *testing.T) {
r.Equal(act.Phase, "Wait")
}
func TestRenderComponent(t *testing.T) {
t.Parallel()
r := require.New(t)
ctx := context.Background()
cuectx := cuecontext.New()
cli := setupClient(ctx, t)
v := cuectx.CompileString(`$params: {
value: {
name: "test-render",
type: "webservice",
}
}`)
r.NoError(v.Err())
mockComponentRender := func(ctx context.Context, comp common.ApplicationComponent, patcher *cue.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, error) {
r.Equal("test-render", comp.Name)
workload := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-workload",
},
},
}
trait := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "test-trait",
"labels": map[string]interface{}{
"trait.oam.dev/resource": "mytrait",
},
},
},
}
return workload, []*unstructured.Unstructured{trait}, nil
}
res, err := RenderComponent(ctx, &oamprovidertypes.Params[cue.Value]{
Params: v,
RuntimeParams: oamprovidertypes.RuntimeParams{
KubeClient: cli,
ComponentRender: mockComponentRender,
},
})
r.NoError(err)
output, err := res.LookupPath(cue.ParsePath("$returns.output.metadata.name")).String()
r.NoError(err)
r.Equal("test-workload", output)
outputs, err := res.LookupPath(cue.ParsePath("$returns.outputs.mytrait.metadata.name")).String()
r.NoError(err)
r.Equal("test-workload", outputs)
}
func TestLoadComponent(t *testing.T) {
t.Parallel()
r := require.New(t)
ctx := context.Background()
act := &mock.Action{}
@ -171,6 +232,7 @@ func TestLoadComponent(t *testing.T) {
}
func TestLoadComponentInOrder(t *testing.T) {
t.Parallel()
r := require.New(t)
ctx := context.Background()
act := &mock.Action{}

View File

@ -15,3 +15,317 @@
*/
package query
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/oam-dev/kubevela/pkg/oam"
querytypes "github.com/oam-dev/kubevela/pkg/utils/types"
)
func TestBuildResourceArray(t *testing.T) {
t.Parallel()
// Define common objects used across tests
pod1 := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "pod1",
},
},
}
pod2 := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "pod2",
"annotations": map[string]interface{}{
oam.AnnotationPublishVersion: "v2.0.0-pod",
oam.AnnotationDeployVersion: "rev2-pod",
},
},
},
}
deployment := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "my-app",
},
},
}
// Define common tree nodes
parentWorkloadNode := &querytypes.ResourceTreeNode{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "my-app",
Namespace: "default",
Object: deployment,
}
pod1Node := &querytypes.ResourceTreeNode{
APIVersion: "v1",
Kind: "Pod",
Name: "pod1",
Namespace: "default",
Object: pod1,
}
pod2Node := &querytypes.ResourceTreeNode{
APIVersion: "v1",
Kind: "Pod",
Name: "pod2",
Namespace: "default",
Object: pod2,
}
replicaSetNode := &querytypes.ResourceTreeNode{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
Name: "my-app-rs",
Namespace: "default",
LeafNodes: []*querytypes.ResourceTreeNode{pod1Node, pod2Node},
}
// Define test cases
testCases := map[string]struct {
res querytypes.AppliedResource
parent *querytypes.ResourceTreeNode
node *querytypes.ResourceTreeNode
kind string
apiVersion string
expected []querytypes.ResourceItem
}{
"simple case with one matching pod": {
res: querytypes.AppliedResource{
Cluster: "local",
Component: "my-comp",
PublishVersion: "v1.0.0",
DeployVersion: "rev1",
},
parent: parentWorkloadNode,
node: pod1Node,
kind: "Pod",
apiVersion: "v1",
expected: []querytypes.ResourceItem{
{
Cluster: "local",
Component: "my-comp",
Workload: querytypes.Workload{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "my-app",
Namespace: "default",
},
Object: pod1,
PublishVersion: "v1.0.0",
DeployVersion: "rev1",
},
},
},
"nested case with multiple matching pods": {
res: querytypes.AppliedResource{
Cluster: "remote",
Component: "my-comp-2",
PublishVersion: "v2.0.0",
DeployVersion: "rev2",
},
parent: parentWorkloadNode,
node: replicaSetNode,
kind: "Pod",
apiVersion: "v1",
expected: []querytypes.ResourceItem{
{
Cluster: "remote",
Component: "my-comp-2",
Workload: querytypes.Workload{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
Name: "my-app-rs",
Namespace: "default",
},
Object: pod1,
PublishVersion: "v2.0.0",
DeployVersion: "rev2",
},
{
Cluster: "remote",
Component: "my-comp-2",
Workload: querytypes.Workload{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
Name: "my-app-rs",
Namespace: "default",
},
Object: pod2,
PublishVersion: "v2.0.0-pod", // From annotation
DeployVersion: "rev2-pod", // From annotation
},
},
},
"no matching nodes": {
res: querytypes.AppliedResource{},
parent: parentWorkloadNode,
node: pod1Node,
kind: "Service",
apiVersion: "v1",
expected: nil,
},
"empty node": {
res: querytypes.AppliedResource{},
parent: parentWorkloadNode,
node: &querytypes.ResourceTreeNode{},
kind: "Pod",
apiVersion: "v1",
expected: nil,
},
"complex tree with mixed resources": {
res: querytypes.AppliedResource{
Cluster: "local",
Component: "my-comp",
PublishVersion: "v1.0.0",
DeployVersion: "rev1",
},
parent: parentWorkloadNode,
node: &querytypes.ResourceTreeNode{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
Name: "my-app-rs",
Namespace: "default",
LeafNodes: []*querytypes.ResourceTreeNode{
pod1Node,
{
APIVersion: "v1",
Kind: "Service",
Name: "my-service",
},
pod2Node,
},
},
kind: "Pod",
apiVersion: "v1",
expected: []querytypes.ResourceItem{
{
Cluster: "local",
Component: "my-comp",
Workload: querytypes.Workload{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
Name: "my-app-rs",
Namespace: "default",
},
Object: pod1,
PublishVersion: "v1.0.0",
DeployVersion: "rev1",
},
{
Cluster: "local",
Component: "my-comp",
Workload: querytypes.Workload{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
Name: "my-app-rs",
Namespace: "default",
},
Object: pod2,
PublishVersion: "v2.0.0-pod",
DeployVersion: "rev2-pod",
},
},
},
"case-insensitive matching": {
res: querytypes.AppliedResource{
Cluster: "local",
Component: "my-comp",
PublishVersion: "v1.0.0",
DeployVersion: "rev1",
},
parent: parentWorkloadNode,
node: pod1Node,
kind: "pod",
apiVersion: "V1",
expected: nil,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
result := buildResourceArray(tc.res, tc.parent, tc.node, tc.kind, tc.apiVersion)
assert.ElementsMatch(t, tc.expected, result, "The returned resource items should match the expected ones")
})
}
}
func TestBuildResourceItem(t *testing.T) {
t.Parallel()
pod := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "test-pod",
},
},
}
podWithAnnotations := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "test-pod-annotated",
"annotations": map[string]interface{}{
oam.AnnotationPublishVersion: "v2.0.0-annotated",
oam.AnnotationDeployVersion: "rev2-annotated",
},
},
},
}
res := querytypes.AppliedResource{
Cluster: "test-cluster",
Component: "test-comp",
PublishVersion: "v1.0.0-res",
DeployVersion: "rev1-res",
}
workload := querytypes.Workload{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "test-workload",
Namespace: "test-ns",
}
t.Run("without annotations", func(t *testing.T) {
t.Parallel()
item := buildResourceItem(res, workload, pod)
assert.Equal(t, "test-cluster", item.Cluster)
assert.Equal(t, "test-comp", item.Component)
assert.Equal(t, workload, item.Workload)
assert.Equal(t, pod, item.Object)
assert.Equal(t, "v1.0.0-res", item.PublishVersion)
assert.Equal(t, "rev1-res", item.DeployVersion)
})
t.Run("with annotations", func(t *testing.T) {
t.Parallel()
item := buildResourceItem(res, workload, podWithAnnotations)
assert.Equal(t, "test-cluster", item.Cluster)
assert.Equal(t, "test-comp", item.Component)
assert.Equal(t, workload, item.Workload)
assert.Equal(t, podWithAnnotations, item.Object)
assert.Equal(t, "v2.0.0-annotated", item.PublishVersion)
assert.Equal(t, "rev2-annotated", item.DeployVersion)
})
t.Run("annotation override", func(t *testing.T) {
item := buildResourceItem(res, workload, podWithAnnotations)
assert.Equal(t, "v2.0.0-annotated", item.PublishVersion)
assert.Equal(t, "rev2-annotated", item.DeployVersion)
})
}

View File

@ -1,114 +0,0 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package appfile
import (
"context"
"os"
"path/filepath"
"testing"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/yaml"
coreoam "github.com/oam-dev/kubevela/apis/core.oam.dev"
corev1beta1 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/system"
// +kubebuilder:scaffold:imports
)
var cfg *rest.Config
var scheme *runtime.Scheme
var k8sClient client.Client
var testEnv *envtest.Environment
var definitionDir string
var wd corev1beta1.WorkloadDefinition
var addonNamespace = "test-addon"
func TestAppFile(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Cli Suite")
}
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
ctx := context.Background()
By("bootstrapping test environment")
useExistCluster := false
testEnv = &envtest.Environment{
ControlPlaneStartTimeout: time.Minute,
ControlPlaneStopTimeout: time.Minute,
CRDDirectoryPaths: []string{filepath.Join("..", "..", "charts", "vela-core", "crds")},
UseExistingCluster: &useExistCluster,
}
var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
scheme = runtime.NewScheme()
Expect(coreoam.AddToScheme(scheme)).NotTo(HaveOccurred())
Expect(clientgoscheme.AddToScheme(scheme)).NotTo(HaveOccurred())
Expect(crdv1.AddToScheme(scheme)).NotTo(HaveOccurred())
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
definitionDir, err = system.GetCapabilityDir()
Expect(err).Should(BeNil())
Expect(os.MkdirAll(definitionDir, 0755)).Should(BeNil())
Expect(k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: addonNamespace}})).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
workloadData, err := os.ReadFile("testdata/workloadDef.yaml")
Expect(err).Should(BeNil())
Expect(yaml.Unmarshal(workloadData, &wd)).Should(BeNil())
wd.Namespace = addonNamespace
logf.Log.Info("Creating workload definition", "data", wd)
Expect(k8sClient.Create(ctx, &wd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
def, err := os.ReadFile("testdata/terraform-aliyun-oss-workloadDefinition.yaml")
Expect(err).Should(BeNil())
var terraformDefinition corev1beta1.WorkloadDefinition
Expect(yaml.Unmarshal(def, &terraformDefinition)).Should(BeNil())
terraformDefinition.Namespace = addonNamespace
logf.Log.Info("Creating workload definition", "data", terraformDefinition)
Expect(k8sClient.Create(ctx, &terraformDefinition)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
})
var _ = AfterSuite(func() {
By("tearing down the test environment")
_ = k8sClient.Delete(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: addonNamespace}})
_ = k8sClient.Delete(context.Background(), &wd)
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})

View File

@ -19,9 +19,9 @@ package appfile
import (
"fmt"
"os"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/assert"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@ -31,7 +31,7 @@ import (
"github.com/oam-dev/kubevela/pkg/utils/util"
)
var _ = It("Test ApplyTerraform", func() {
func TestApplyTerraform(t *testing.T) {
app := &v1beta1.Application{
ObjectMeta: v1.ObjectMeta{Name: "test-terraform-app"},
Spec: v1beta1.ApplicationSpec{Components: []commontype.ApplicationComponent{{
@ -46,27 +46,29 @@ var _ = It("Test ApplyTerraform", func() {
Schema: scheme,
}
err := arg.SetConfig(cfg)
Expect(err).Should(BeNil())
assert.NoError(t, err)
_, err = ApplyTerraform(app, k8sClient, ioStream, addonNamespace, arg)
Expect(err).Should(BeNil())
})
assert.NoError(t, err)
}
var _ = Describe("Test generateSecretFromTerraformOutput", func() {
func TestGenerateSecretFromTerraformOutput(t *testing.T) {
var name = "test-addon-secret"
It("namespace doesn't exist", func() {
t.Run("namespace doesn't exist", func(t *testing.T) {
badNamespace := "a-not-existed-namespace"
err := generateSecretFromTerraformOutput(k8sClient, "", name, badNamespace)
Expect(err).Should(Equal(fmt.Errorf("namespace %s doesn't exist", badNamespace)))
})
It("valid output list", func() {
rawOutput := "name=aaa\nage=1"
err := generateSecretFromTerraformOutput(k8sClient, rawOutput, name, addonNamespace)
Expect(err).Should(BeNil())
assert.EqualError(t, err, fmt.Sprintf("namespace %s doesn't exist", badNamespace))
})
It("invalid output list", func() {
t.Run("valid output list", func(t *testing.T) {
rawOutput := "name=aaa\nage=1"
err := generateSecretFromTerraformOutput(k8sClient, rawOutput, name, addonNamespace)
assert.NoError(t, err)
})
t.Run("invalid output list", func(t *testing.T) {
rawOutput := "name"
err := generateSecretFromTerraformOutput(k8sClient, rawOutput, name, addonNamespace)
Expect(err).Should(Equal(fmt.Errorf("terraform output isn't in the right format: %q", rawOutput)))
assert.EqualError(t, err, fmt.Sprintf("terraform output isn't in the right format: %q", rawOutput))
})
})
}

View File

@ -34,6 +34,58 @@ import (
"github.com/oam-dev/kubevela/references/appfile/template"
)
func TestNewAppFile(t *testing.T) {
appFile := NewAppFile()
assert.NotNil(t, appFile)
assert.NotNil(t, appFile.Services)
assert.NotNil(t, appFile.Secrets)
assert.Empty(t, appFile.Name)
}
func TestJSONToYaml(t *testing.T) {
t.Run("valid json", func(t *testing.T) {
jsonData := `{"name":"test-app","services":{"my-svc":{"image":"nginx"}}}`
appFile := NewAppFile()
_, err := JSONToYaml([]byte(jsonData), appFile)
assert.NoError(t, err)
assert.Equal(t, "test-app", appFile.Name)
assert.Contains(t, appFile.Services, "my-svc")
assert.Equal(t, "nginx", appFile.Services["my-svc"]["image"])
})
t.Run("invalid json", func(t *testing.T) {
jsonData := `{"name":"test-app"`
appFile := NewAppFile()
_, err := JSONToYaml([]byte(jsonData), appFile)
assert.Error(t, err)
})
}
func TestLoadFromBytes(t *testing.T) {
t.Run("valid yaml", func(t *testing.T) {
yamlData := `
name: test-yaml-app
services:
main:
image: httpd
`
appFile, err := LoadFromBytes([]byte(yamlData))
assert.NoError(t, err)
assert.Equal(t, "test-yaml-app", appFile.Name)
assert.Len(t, appFile.Services, 1)
assert.Equal(t, "httpd", appFile.Services["main"]["image"])
})
t.Run("valid json", func(t *testing.T) {
jsonData := `{"name":"test-json-app","services":{"sidecar":{"image":"redis"}}}`
appFile, err := LoadFromBytes([]byte(jsonData))
assert.NoError(t, err)
assert.Equal(t, "test-json-app", appFile.Name)
assert.Len(t, appFile.Services, 1)
assert.Equal(t, "redis", appFile.Services["sidecar"]["image"])
})
}
func TestBuildOAMApplication2(t *testing.T) {
expectNs := "test-ns"

View File

@ -36,3 +36,85 @@ func TestGetType(t *testing.T) {
got = svc2.GetType()
assert.Equal(t, workload2, got)
}
func TestGetUserConfigName(t *testing.T) {
t.Run("config name exists", func(t *testing.T) {
svc := Service{"config": "my-config"}
assert.Equal(t, "my-config", svc.GetUserConfigName())
})
t.Run("config name does not exist", func(t *testing.T) {
svc := Service{"image": "nginx"}
assert.Equal(t, "", svc.GetUserConfigName())
})
}
func TestGetApplicationConfig(t *testing.T) {
svc := Service{
"image": "nginx",
"port": 80,
"type": "webservice",
"build": "./",
"config": "my-config",
}
config := svc.GetApplicationConfig()
assert.Contains(t, config, "image")
assert.Contains(t, config, "port")
assert.NotContains(t, config, "type")
assert.NotContains(t, config, "build")
assert.NotContains(t, config, "config")
assert.Len(t, config, 2)
}
func TestToStringSlice(t *testing.T) {
testCases := []struct {
name string
input interface{}
expected []string
}{
{
name: "string",
input: "one",
expected: []string{"one"},
},
{
name: "[]string",
input: []string{"one", "two"},
expected: []string{"one", "two"},
},
{
name: "[]interface{} of strings",
input: []interface{}{"one", "two"},
expected: []string{"one", "two"},
},
{
name: "[]interface{} of mixed types",
input: []interface{}{"one", 2, "three"},
expected: []string{"one", "three"},
},
{
name: "nil input",
input: nil,
expected: nil,
},
{
name: "empty []string",
input: []string{},
expected: []string{},
},
{
name: "other type (int)",
input: 123,
expected: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := toStringSlice(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}

View File

@ -22,14 +22,18 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/yaml"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/references/appfile/api"
"github.com/oam-dev/kubevela/references/appfile/template"
)
func TestApplication(t *testing.T) {
yamlNormal := `name: myapp
const (
yamlNormal = `name: myapp
services:
frontend:
image: inanimate/echo-server
@ -45,55 +49,207 @@ services:
type: cloneset
image: "back:v1"
`
yamlNoService := `name: myapp`
yamlNoName := `services:
yamlNoService = `name: myapp`
yamlNoName = `services:
frontend:
image: inanimate/echo-server
env:
PORT: 8080`
yamlTraitNotMap := `name: myapp
yamlTraitNotMap = `name: myapp
services:
frontend:
image: inanimate/echo-server
env:
PORT: 8080
autoscaling: 10`
)
cases := map[string]struct {
raw string
InValid bool
InvalidReason error
ExpName string
ExpComponents []string
WantWorkload string
ExpWorkload map[string]interface{}
ExpWorkloadType string
ExpTraits map[string]map[string]interface{}
func TestNewApplication(t *testing.T) {
tm := template.NewFakeTemplateManager()
app := NewApplication(nil, tm)
assert.NotNil(t, app)
assert.Equal(t, tm, app.Tm)
assert.NotNil(t, app.AppFile)
appfile := api.NewAppFile()
appfile.Name = "test-app"
app = NewApplication(appfile, tm)
assert.NotNil(t, app)
assert.Equal(t, "test-app", app.Name)
}
func TestValidate(t *testing.T) {
testCases := map[string]struct {
raw string
expErr error
addFake bool
}{
"normal case backend": {
raw: yamlNormal,
ExpName: "myapp",
ExpComponents: []string{"backend", "frontend"},
WantWorkload: "backend",
ExpWorkload: map[string]interface{}{
"image": "back:v1",
},
ExpWorkloadType: "cloneset",
ExpTraits: map[string]map[string]interface{}{},
"normal": {
raw: yamlNormal,
expErr: nil,
},
"normal case frontend": {
raw: yamlNormal,
ExpName: "myapp",
ExpComponents: []string{"backend", "frontend"},
WantWorkload: "frontend",
ExpWorkload: map[string]interface{}{
"no service": {
raw: yamlNoService,
expErr: errors.New("at least one service is required"),
},
"no name": {
raw: yamlNoName,
expErr: errors.New("name is required"),
},
"trait not map": {
raw: yamlTraitNotMap,
expErr: fmt.Errorf("trait autoscaling in 'frontend' must be map"),
addFake: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
tm := template.NewFakeTemplateManager()
if tc.addFake {
tm.Templates["autoscaling"] = &template.Template{
Captype: types.TypeTrait,
}
}
app := NewApplication(nil, tm)
err := yaml.Unmarshal([]byte(tc.raw), &app)
assert.NoError(t, err)
err = Validate(app)
assert.Equal(t, tc.expErr, err)
})
}
}
func TestGetComponents(t *testing.T) {
app := &v1beta1.Application{
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{Name: "c"},
{Name: "a"},
{Name: "b"},
},
},
}
comps := GetComponents(app)
assert.Equal(t, []string{"a", "b", "c"}, comps)
}
func TestGetServiceConfig(t *testing.T) {
tm := template.NewFakeTemplateManager()
app := NewApplication(nil, tm)
err := yaml.Unmarshal([]byte(yamlNormal), &app)
assert.NoError(t, err)
tp, cfg := GetServiceConfig(app, "frontend")
assert.Equal(t, "webservice", tp)
assert.NotEmpty(t, cfg)
assert.Contains(t, cfg, "image")
tp, cfg = GetServiceConfig(app, "backend")
assert.Equal(t, "cloneset", tp)
assert.NotEmpty(t, cfg)
assert.Contains(t, cfg, "image")
tp, cfg = GetServiceConfig(app, "non-existent")
assert.Equal(t, "", tp)
assert.Empty(t, cfg)
}
func TestGetApplicationSettings(t *testing.T) {
app := &v1beta1.Application{
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "comp-1",
Type: "worker",
Properties: &runtime.RawExtension{
Raw: []byte(`{"image":"my-image"}`),
},
},
},
},
}
tp, settings := GetApplicationSettings(app, "comp-1")
assert.Equal(t, "worker", tp)
assert.Equal(t, map[string]interface{}{"image": "my-image"}, settings)
tp, settings = GetApplicationSettings(app, "non-existent")
assert.Equal(t, "", tp)
assert.Empty(t, settings)
}
func TestGetWorkload(t *testing.T) {
tm := template.NewFakeTemplateManager()
tm.Templates["autoscaling"] = &template.Template{Captype: types.TypeTrait}
tm.Templates["rollout"] = &template.Template{Captype: types.TypeTrait}
app := NewApplication(nil, tm)
err := yaml.Unmarshal([]byte(yamlNormal), &app)
assert.NoError(t, err)
testCases := map[string]struct {
componentName string
expWorkloadType string
expWorkload map[string]interface{}
}{
"frontend": {
componentName: "frontend",
expWorkloadType: "webservice",
expWorkload: map[string]interface{}{
"image": "inanimate/echo-server",
"env": map[string]interface{}{
"PORT": float64(8080),
},
},
ExpWorkloadType: "webservice",
ExpTraits: map[string]map[string]interface{}{
},
"backend": {
componentName: "backend",
expWorkloadType: "cloneset",
expWorkload: map[string]interface{}{
"image": "back:v1",
},
},
"non-existent": {
componentName: "non-existent",
expWorkloadType: "",
expWorkload: map[string]interface{}{},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
workloadType, workload := GetWorkload(app, tc.componentName)
assert.Equal(t, tc.expWorkloadType, workloadType)
assert.Equal(t, tc.expWorkload, workload)
})
}
}
func TestGetTraits(t *testing.T) {
tm := template.NewFakeTemplateManager()
tm.Templates["autoscaling"] = &template.Template{Captype: types.TypeTrait}
tm.Templates["rollout"] = &template.Template{Captype: types.TypeTrait}
app := NewApplication(nil, tm)
err := yaml.Unmarshal([]byte(yamlNormal), &app)
assert.NoError(t, err)
// Test case with invalid trait format
invalidTraitApp := NewApplication(nil, tm)
err = yaml.Unmarshal([]byte(yamlTraitNotMap), &invalidTraitApp)
assert.NoError(t, err)
testCases := map[string]struct {
app *api.Application
compName string
exp map[string]map[string]interface{}
expErr string
}{
"frontend traits": {
app: app,
compName: "frontend",
exp: map[string]map[string]interface{}{
"autoscaling": {
"max": float64(10),
"min": float64(1),
@ -104,50 +260,33 @@ services:
},
},
},
"no component": {
raw: yamlNoService,
ExpName: "myapp",
InValid: true,
InvalidReason: errors.New("at least one service is required"),
"backend traits (none)": {
app: app,
compName: "backend",
exp: map[string]map[string]interface{}{},
},
"no name": {
raw: yamlNoName,
ExpName: "",
InValid: true,
InvalidReason: errors.New("name is required"),
"non-existent component": {
app: app,
compName: "non-existent",
exp: map[string]map[string]interface{}{},
},
"trait must be map": {
raw: yamlTraitNotMap,
ExpTraits: map[string]map[string]interface{}{
"autoscaling": {},
},
ExpName: "myapp",
InValid: true,
InvalidReason: fmt.Errorf("trait autoscaling in 'frontend' must be map"),
"invalid trait format": {
app: invalidTraitApp,
compName: "frontend",
exp: nil,
expErr: "autoscaling is trait, but with invalid format float64, should be map[string]interface{}",
},
}
for caseName, c := range cases {
tm := template.NewFakeTemplateManager()
for k := range c.ExpTraits {
tm.Templates[k] = &template.Template{
Captype: types.TypeTrait,
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
traits, err := GetTraits(tc.app, tc.compName)
if tc.expErr != "" {
assert.EqualError(t, err, tc.expErr)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.exp, traits)
}
}
app := NewApplication(nil, tm)
err := yaml.Unmarshal([]byte(c.raw), &app)
assert.NoError(t, err, caseName)
err = Validate(app)
if c.InValid {
assert.Equal(t, c.InvalidReason, err)
continue
}
assert.Equal(t, c.ExpName, app.Name, caseName)
workloadType, workload := GetWorkload(app, c.WantWorkload)
assert.Equal(t, c.ExpWorkload, workload, caseName)
assert.Equal(t, c.ExpWorkloadType, workloadType, caseName)
traits, err := GetTraits(app, c.WantWorkload)
assert.NoError(t, err, caseName)
assert.Equal(t, c.ExpTraits, traits, caseName)
})
}
}

View File

@ -0,0 +1,161 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package appfile
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/yaml"
coreoam "github.com/oam-dev/kubevela/apis/core.oam.dev"
corev1beta1 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/utils/system"
)
var cfg *rest.Config
var scheme *runtime.Scheme
var k8sClient client.Client
var testEnv *envtest.Environment
var definitionDir string
var wd corev1beta1.WorkloadDefinition
var addonNamespace = "test-addon"
func TestMain(m *testing.M) {
logf.SetLogger(zap.New(zap.UseDevMode(true)))
ctx := context.Background()
useExistCluster := false
testEnv = &envtest.Environment{
ControlPlaneStartTimeout: time.Minute,
ControlPlaneStopTimeout: time.Minute,
CRDDirectoryPaths: []string{filepath.Join("..", "..", "charts", "vela-core", "crds")},
UseExistingCluster: &useExistCluster,
}
var err error
cfg, err = testEnv.Start()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to start test environment: %v\n", err)
os.Exit(1)
}
// cleanupAndExit stops the test environment and exits with the given code.
cleanupAndExit := func(code int) {
// Clean up other resources before stopping the environment
if k8sClient != nil {
_ = k8sClient.Delete(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: addonNamespace}})
_ = k8sClient.Delete(context.Background(), &wd)
}
if err := testEnv.Stop(); err != nil {
fmt.Fprintf(os.Stderr, "Failed to stop test environment: %v\n", err)
}
os.Exit(code)
}
scheme = runtime.NewScheme()
if err := coreoam.AddToScheme(scheme); err != nil {
fmt.Fprintf(os.Stderr, "Failed to add coreoam to scheme: %v\n", err)
cleanupAndExit(1)
}
if err := clientgoscheme.AddToScheme(scheme); err != nil {
fmt.Fprintf(os.Stderr, "Failed to add clientgoscheme to scheme: %v\n", err)
cleanupAndExit(1)
}
if err := crdv1.AddToScheme(scheme); err != nil {
fmt.Fprintf(os.Stderr, "Failed to add crdv1 to scheme: %v\n", err)
cleanupAndExit(1)
}
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create k8sClient: %v\n", err)
cleanupAndExit(1)
}
definitionDir, err = system.GetCapabilityDir()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get capability dir: %v\n", err)
cleanupAndExit(1)
}
if err := os.MkdirAll(definitionDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create capability dir: %v\n", err)
cleanupAndExit(1)
}
if err := k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: addonNamespace}}); err != nil {
if !errors.IsAlreadyExists(err) {
fmt.Fprintf(os.Stderr, "Failed to create test namespace: %v\n", err)
cleanupAndExit(1)
}
}
workloadData, err := os.ReadFile("testdata/workloadDef.yaml")
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to read workloadDef.yaml: %v\n", err)
cleanupAndExit(1)
}
if err := yaml.Unmarshal(workloadData, &wd); err != nil {
fmt.Fprintf(os.Stderr, "Failed to unmarshal workloadDef.yaml: %v\n", err)
cleanupAndExit(1)
}
wd.Namespace = addonNamespace
if err := k8sClient.Create(ctx, &wd); err != nil {
if !errors.IsAlreadyExists(err) {
fmt.Fprintf(os.Stderr, "Failed to create workload definition: %v\n", err)
cleanupAndExit(1)
}
}
def, err := os.ReadFile("testdata/terraform-aliyun-oss-workloadDefinition.yaml")
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to read terraform-aliyun-oss-workloadDefinition.yaml: %v\n", err)
cleanupAndExit(1)
}
var terraformDefinition corev1beta1.WorkloadDefinition
if err := yaml.Unmarshal(def, &terraformDefinition); err != nil {
fmt.Fprintf(os.Stderr, "Failed to unmarshal terraformDefinition: %v\n", err)
cleanupAndExit(1)
}
terraformDefinition.Namespace = addonNamespace
if err := k8sClient.Create(ctx, &terraformDefinition); err != nil {
if !errors.IsAlreadyExists(err) {
fmt.Fprintf(os.Stderr, "Failed to create terraform workload definition: %v\n", err)
cleanupAndExit(1)
}
}
code := m.Run()
cleanupAndExit(code)
}

View File

@ -0,0 +1,91 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package appfile
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/oam-dev/kubevela/references/appfile/api"
"github.com/oam-dev/kubevela/references/appfile/template"
)
func TestSetWorkload(t *testing.T) {
tm := template.NewFakeTemplateManager()
t.Run("app is nil", func(t *testing.T) {
err := SetWorkload(nil, "comp", "worker", nil)
assert.EqualError(t, err, errorAppNilPointer.Error())
})
t.Run("add new component", func(t *testing.T) {
app := NewApplication(nil, tm)
app.Name = "test-app"
workloadData := map[string]interface{}{"image": "test-image", "cmd": []string{"sleep", "1000"}}
err := SetWorkload(app, "my-comp", "worker", workloadData)
assert.NoError(t, err)
assert.Len(t, app.Services, 1)
svc, ok := app.Services["my-comp"]
assert.True(t, ok)
assert.NotNil(t, svc)
assert.Equal(t, "worker", svc["type"])
assert.Equal(t, "test-image", svc["image"])
assert.Equal(t, []string{"sleep", "1000"}, svc["cmd"])
})
t.Run("update existing component", func(t *testing.T) {
app := NewApplication(nil, tm)
app.Name = "test-app"
app.Services["my-comp"] = api.Service{
"type": "worker",
"image": "initial-image",
}
updatedWorkloadData := map[string]interface{}{"image": "updated-image", "port": 8080}
err := SetWorkload(app, "my-comp", "webservice", updatedWorkloadData)
assert.NoError(t, err)
assert.Len(t, app.Services, 1)
svc, ok := app.Services["my-comp"]
assert.True(t, ok)
assert.NotNil(t, svc)
assert.Equal(t, "webservice", svc["type"])
assert.Equal(t, "updated-image", svc["image"])
assert.Equal(t, 8080, svc["port"])
})
t.Run("add to existing services", func(t *testing.T) {
app := NewApplication(nil, tm)
app.Name = "test-app"
app.Services["comp-1"] = api.Service{
"type": "worker",
}
workloadData := map[string]interface{}{"image": "test-image"}
err := SetWorkload(app, "comp-2", "task", workloadData)
assert.NoError(t, err)
assert.Len(t, app.Services, 2)
assert.Contains(t, app.Services, "comp-1")
assert.Contains(t, app.Services, "comp-2")
svc2 := app.Services["comp-2"]
assert.Equal(t, "task", svc2["type"])
assert.Equal(t, "test-image", svc2["image"])
})
}

View File

@ -0,0 +1,161 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package appfile
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam"
)
func TestCreateOrUpdateApplication(t *testing.T) {
scheme := runtime.NewScheme()
assert.NoError(t, v1beta1.AddToScheme(scheme))
app := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "default",
},
}
t.Run("create application", func(t *testing.T) {
builder := fake.NewClientBuilder().WithScheme(scheme)
fakeClient := builder.Build()
err := CreateOrUpdateApplication(context.Background(), fakeClient, app)
assert.NoError(t, err)
var created v1beta1.Application
err = fakeClient.Get(context.Background(), client.ObjectKeyFromObject(app), &created)
assert.NoError(t, err)
assert.Equal(t, "test-app", created.Name)
})
t.Run("update application", func(t *testing.T) {
appToUpdate := app.DeepCopy()
appToUpdate.SetAnnotations(map[string]string{"key": "val"})
builder := fake.NewClientBuilder().WithScheme(scheme).WithObjects(app)
fakeClient := builder.Build()
err := CreateOrUpdateApplication(context.Background(), fakeClient, appToUpdate)
assert.NoError(t, err)
var updated v1beta1.Application
err = fakeClient.Get(context.Background(), client.ObjectKeyFromObject(app), &updated)
assert.NoError(t, err)
assert.Equal(t, "val", updated.Annotations["key"])
})
}
func TestCreateOrUpdateObjects(t *testing.T) {
scheme := runtime.NewScheme()
assert.NoError(t, corev1.AddToScheme(scheme))
cm := &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "default",
},
Data: map[string]string{
"initial": "true",
},
}
t.Run("create object", func(t *testing.T) {
builder := fake.NewClientBuilder().WithScheme(scheme)
fakeClient := builder.Build()
objects := []oam.Object{cm}
err := CreateOrUpdateObjects(context.Background(), fakeClient, objects)
assert.NoError(t, err)
var created corev1.ConfigMap
err = fakeClient.Get(context.Background(), client.ObjectKeyFromObject(cm), &created)
assert.NoError(t, err)
assert.Equal(t, "true", created.Data["initial"])
})
t.Run("update object", func(t *testing.T) {
cmToUpdate := cm.DeepCopy()
cmToUpdate.Data["initial"] = "false"
builder := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cm)
fakeClient := builder.Build()
objects := []oam.Object{cmToUpdate}
err := CreateOrUpdateObjects(context.Background(), fakeClient, objects)
assert.NoError(t, err)
var updated corev1.ConfigMap
err = fakeClient.Get(context.Background(), client.ObjectKeyFromObject(cm), &updated)
assert.NoError(t, err)
assert.Equal(t, "false", updated.Data["initial"])
})
}
func TestRun(t *testing.T) {
scheme := runtime.NewScheme()
assert.NoError(t, v1beta1.AddToScheme(scheme))
assert.NoError(t, corev1.AddToScheme(scheme))
app := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app-run",
Namespace: "default",
},
}
cm := &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm-run",
Namespace: "default",
},
}
builder := fake.NewClientBuilder().WithScheme(scheme)
fakeClient := builder.Build()
err := Run(context.Background(), fakeClient, app, []oam.Object{cm})
assert.NoError(t, err)
var createdApp v1beta1.Application
err = fakeClient.Get(context.Background(), client.ObjectKeyFromObject(app), &createdApp)
assert.NoError(t, err)
assert.Equal(t, "test-app-run", createdApp.Name)
var createdCM corev1.ConfigMap
err = fakeClient.Get(context.Background(), client.ObjectKeyFromObject(cm), &createdCM)
assert.NoError(t, err)
assert.Equal(t, "test-cm-run", createdCM.Name)
}

View File

@ -18,6 +18,7 @@ package config
import (
"os"
"path/filepath"
"strings"
"testing"
@ -27,15 +28,82 @@ import (
)
func TestString(t *testing.T) {
c := Color("red")
assert.Equal(t, c.String(), "#ff0000")
testCases := map[string]struct {
color Color
expected string
}{
"named color": {
color: "red",
expected: "#ff0000",
},
"hex color": {
color: "#aabbcc",
expected: "#aabbcc",
},
"default color": {
color: DefaultColor,
expected: "-",
},
"invalid color": {
color: "invalidColor",
expected: "-",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.color.String())
})
}
}
func TestColor(t *testing.T) {
c1 := Color("#ff0000")
assert.Equal(t, c1.Color(), tcell.GetColor("#ff0000"))
assert.Equal(t, tcell.GetColor("#ff0000"), c1.Color())
c2 := Color("red")
assert.Equal(t, c2.Color(), tcell.GetColor("red").TrueColor())
assert.Equal(t, tcell.GetColor("red").TrueColor(), c2.Color())
c3 := Color(DefaultColor)
assert.Equal(t, tcell.ColorDefault, c3.Color())
}
func TestIsHex(t *testing.T) {
testCases := map[string]struct {
color Color
expected bool
}{
"is hex": {
color: "#aabbcc",
expected: true,
},
"not hex (too short)": {
color: "#aabbc",
expected: false,
},
"not hex (too long)": {
color: "#aabbccd",
expected: false,
},
"not hex (no #)": {
color: "aabbcc",
expected: false,
},
"not hex (named color)": {
color: "red",
expected: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.color.isHex())
})
}
}
func TestDefaultTheme(t *testing.T) {
theme := defaultTheme()
assert.NotNil(t, theme)
assert.Equal(t, Color("royalblue"), theme.Info.Title)
assert.Equal(t, Color("green"), theme.Status.Healthy)
assert.Equal(t, Color("red"), theme.Status.UnHealthy)
}
func TestPersistentThemeConfig(t *testing.T) {
@ -45,3 +113,101 @@ func TestPersistentThemeConfig(t *testing.T) {
assert.Nil(t, err)
assert.True(t, strings.Contains(string(bytes), "foo"))
}
func TestMakeThemeConfigFileIfNotExist(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "vela-theme-test")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
originalHomePath := homePath
originalThemeConfigFilePath := themeConfigFilePath
homePath = tmpDir
themeConfigFilePath = filepath.Join(tmpDir, themeHomeDirPath, themeConfigFile)
defer func() {
homePath = originalHomePath
themeConfigFilePath = originalThemeConfigFilePath
}()
t.Run("should create file if it does not exist", func(t *testing.T) {
os.Remove(themeConfigFilePath)
exists := makeThemeConfigFileIfNotExist()
assert.False(t, exists, "should return false as file was created")
_, err := os.Stat(themeConfigFilePath)
assert.NoError(t, err, "expected theme config file to be created")
content, err := os.ReadFile(themeConfigFilePath)
assert.NoError(t, err)
assert.Equal(t, "name : "+DefaultTheme, string(content))
})
t.Run("should not modify file if it already exists", func(t *testing.T) {
customContent := "name : custom"
err := os.WriteFile(themeConfigFilePath, []byte(customContent), 0600)
assert.NoError(t, err)
exists := makeThemeConfigFileIfNotExist()
assert.True(t, exists, "should return true as file already exists")
content, err := os.ReadFile(themeConfigFilePath)
assert.NoError(t, err)
assert.Equal(t, customContent, string(content))
})
}
func TestLoadThemeConfig(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "vela-theme-test-load")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
originalHomePath := homePath
originalThemeConfigFilePath := themeConfigFilePath
originalDiyThemeDirPath := diyThemeDirPath
homePath = tmpDir
themeConfigFilePath = filepath.Join(tmpDir, themeHomeDirPath, themeConfigFile)
diyThemeDirPath = filepath.Join(tmpDir, themeHomeDirPath, diyThemeDir)
defer func() {
homePath = originalHomePath
themeConfigFilePath = originalThemeConfigFilePath
diyThemeDirPath = originalDiyThemeDirPath
}()
ThemeMap["custom"] = ThemeConfig{
Info: struct {
Title Color `yaml:"title"`
Text Color `yaml:"text"`
}{Title: "custom-title"},
}
defer delete(ThemeMap, "custom")
t.Run("config file not exist", func(t *testing.T) {
os.Remove(themeConfigFilePath)
cfg := LoadThemeConfig()
assert.Equal(t, defaultTheme().Info.Title, cfg.Info.Title)
})
t.Run("config file with default theme", func(t *testing.T) {
PersistentThemeConfig(DefaultTheme)
cfg := LoadThemeConfig()
assert.Equal(t, defaultTheme().Info.Title, cfg.Info.Title)
})
t.Run("config file with custom theme", func(t *testing.T) {
PersistentThemeConfig("custom")
cfg := LoadThemeConfig()
assert.Equal(t, Color("custom-title"), cfg.Info.Title)
})
t.Run("config file with unknown theme", func(t *testing.T) {
PersistentThemeConfig("unknown")
cfg := LoadThemeConfig()
assert.Equal(t, defaultTheme().Info.Title, cfg.Info.Title)
})
t.Run("config file with invalid content", func(t *testing.T) {
err := os.WriteFile(themeConfigFilePath, []byte("name: [invalid"), 0600)
assert.NoError(t, err)
cfg := LoadThemeConfig()
assert.Equal(t, defaultTheme().Info.Title, cfg.Info.Title)
})
}

View File

@ -20,15 +20,14 @@ import (
"context"
"testing"
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/assert"
)
func TestApplicationList_ToTableBody(t *testing.T) {
appList := &ApplicationList{{"Name", "Namespace", "Phase", "", "", "", "CreateTime"}}
assert.Equal(t, appList.ToTableBody(), [][]string{{"Name", "Namespace", "Phase", "", "", "", "CreateTime"}})
}
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
)
var _ = Describe("test Application", func() {
ctx := context.Background()
@ -66,3 +65,164 @@ var _ = Describe("test Application", func() {
Expect(len(topology)).To(Equal(4))
})
})
func TestApplicationList_ToTableBody(t *testing.T) {
testCases := []struct {
name string
list ApplicationList
expected [][]string
}{
{
name: "empty list",
list: ApplicationList{},
expected: make([][]string, 0),
},
{
name: "single item list",
list: ApplicationList{
{name: "app1", namespace: "ns1", phase: "running", workflowMode: "DAG", workflow: "1/1", service: "1/1", createTime: "now"},
},
expected: [][]string{
{"app1", "ns1", "running", "DAG", "1/1", "1/1", "now"},
},
},
{
name: "multiple item list",
list: ApplicationList{
{name: "app1", namespace: "ns1", phase: "running", workflowMode: "DAG", workflow: "1/1", service: "1/1", createTime: "now"},
{name: "app2", namespace: "ns2", phase: "failed", workflowMode: "StepByStep", workflow: "0/1", service: "0/1", createTime: "then"},
},
expected: [][]string{
{"app1", "ns1", "running", "DAG", "1/1", "1/1", "now"},
{"app2", "ns2", "failed", "StepByStep", "0/1", "0/1", "then"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := tc.list.ToTableBody()
if len(tc.expected) == 0 {
assert.Empty(t, result)
} else {
assert.Equal(t, tc.expected, result)
}
})
}
}
func TestServiceNum(t *testing.T) {
testCases := []struct {
name string
app v1beta1.Application
expected string
}{
{
name: "no services",
app: v1beta1.Application{Status: common.AppStatus{Services: []common.ApplicationComponentStatus{}}},
expected: "0/0",
},
{
name: "one healthy, one unhealthy",
app: v1beta1.Application{Status: common.AppStatus{Services: []common.ApplicationComponentStatus{
{Healthy: true},
{Healthy: false},
}}},
expected: "1/2",
},
{
name: "all healthy",
app: v1beta1.Application{Status: common.AppStatus{Services: []common.ApplicationComponentStatus{
{Healthy: true},
{Healthy: true},
}}},
expected: "2/2",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, serviceNum(tc.app))
})
}
}
func TestWorkflowMode(t *testing.T) {
testCases := []struct {
name string
app v1beta1.Application
expected string
}{
{
name: "workflow is nil",
app: v1beta1.Application{Status: common.AppStatus{Workflow: nil}},
expected: Unknown,
},
{
name: "workflow mode is DAG",
app: v1beta1.Application{Status: common.AppStatus{Workflow: &common.WorkflowStatus{Mode: "DAG"}}},
expected: "DAG",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, workflowMode(tc.app))
})
}
}
func TestWorkflowStepNum(t *testing.T) {
testCases := []struct {
name string
app v1beta1.Application
expected string
}{
{
name: "workflow is nil",
app: v1beta1.Application{Status: common.AppStatus{Workflow: nil}},
expected: "N/A",
},
{
name: "empty workflow steps",
app: v1beta1.Application{Status: common.AppStatus{Workflow: &common.WorkflowStatus{
Steps: []workflowv1alpha1.WorkflowStepStatus{},
}}},
expected: "0/0",
},
{
name: "all steps succeeded",
app: v1beta1.Application{Status: common.AppStatus{Workflow: &common.WorkflowStatus{
Steps: []workflowv1alpha1.WorkflowStepStatus{
{StepStatus: workflowv1alpha1.StepStatus{Phase: workflowv1alpha1.WorkflowStepPhaseSucceeded}},
{StepStatus: workflowv1alpha1.StepStatus{Phase: workflowv1alpha1.WorkflowStepPhaseSucceeded}},
},
}}},
expected: "2/2",
},
{
name: "some steps succeeded, some failed/running",
app: v1beta1.Application{Status: common.AppStatus{Workflow: &common.WorkflowStatus{
Steps: []workflowv1alpha1.WorkflowStepStatus{
{StepStatus: workflowv1alpha1.StepStatus{Phase: workflowv1alpha1.WorkflowStepPhaseSucceeded}},
{StepStatus: workflowv1alpha1.StepStatus{Phase: workflowv1alpha1.WorkflowStepPhaseFailed}},
{StepStatus: workflowv1alpha1.StepStatus{Phase: workflowv1alpha1.WorkflowStepPhaseRunning}},
},
}}},
expected: "1/3",
},
{
name: "all steps failed/running",
app: v1beta1.Application{Status: common.AppStatus{Workflow: &common.WorkflowStatus{
Steps: []workflowv1alpha1.WorkflowStepStatus{
{StepStatus: workflowv1alpha1.StepStatus{Phase: workflowv1alpha1.WorkflowStepPhaseFailed}},
{StepStatus: workflowv1alpha1.StepStatus{Phase: workflowv1alpha1.WorkflowStepPhaseRunning}},
},
}}},
expected: "0/2",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, workflowStepNum(tc.app))
})
}
}

View File

@ -24,19 +24,43 @@ import (
)
func TestTimeFormat(t *testing.T) {
t1, err1 := time.ParseDuration("1.5h")
assert.NoError(t, err1)
assert.Equal(t, TimeFormat(t1), "1h30m0s")
t2, err2 := time.ParseDuration("25h")
assert.NoError(t, err2)
assert.Equal(t, TimeFormat(t2), "1d1h0m0s")
t3, err3 := time.ParseDuration("0.1h")
assert.NoError(t, err3)
assert.Equal(t, TimeFormat(t3), "6m0s")
t4, err4 := time.ParseDuration("0.001h")
assert.NoError(t, err4)
assert.Equal(t, TimeFormat(t4), "3s")
t5, err5 := time.ParseDuration("0.00001h")
assert.NoError(t, err5)
assert.Equal(t, TimeFormat(t5), "36ms")
testCases := []struct {
name string
in string
expected string
}{
{
name: "1.5h",
in: "1.5h",
expected: "1h30m0s",
},
{
name: "25h",
in: "25h",
expected: "1d1h0m0s",
},
{
name: "0.1h",
in: "0.1h",
expected: "6m0s",
},
{
name: "0.001h",
in: "0.001h",
expected: "3s",
},
{
name: "0.00001h",
in: "0.00001h",
expected: "36ms",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
d, err := time.ParseDuration(tc.in)
assert.NoError(t, err)
assert.Equal(t, tc.expected, TimeFormat(d))
})
}
}

View File

@ -19,11 +19,16 @@ package cuegen
import (
"bytes"
goast "go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
"os"
"path/filepath"
"strings"
"testing"
cueast "cuelang.org/go/cue/ast"
"github.com/stretchr/testify/assert"
)
@ -88,3 +93,200 @@ func TestConvertNullable(t *testing.T) {
assert.Equal(t, got.String(), string(want))
}
func TestMakeComment(t *testing.T) {
cases := []struct {
name string
in *goast.CommentGroup
out []string
}{
{
name: "nil comment",
in: nil,
out: nil,
},
{
name: "empty comment",
in: &goast.CommentGroup{},
out: nil,
},
{
name: "line comment",
in: &goast.CommentGroup{
List: []*goast.Comment{
{Text: "// hello"},
{Text: "// world"},
},
},
out: []string{"// hello", "// world"},
},
{
name: "block comment",
in: &goast.CommentGroup{
List: []*goast.Comment{
{Text: "/* hello world */"},
},
},
out: []string{"// hello world "},
},
{
name: "multiline block comment",
in: &goast.CommentGroup{
List: []*goast.Comment{
{Text: `/*
* hello
* world
*/`},
},
},
out: []string{"// * hello", "// * world", "//"},
},
{
name: "multiline block comment with no space",
in: &goast.CommentGroup{
List: []*goast.Comment{
{Text: `/*
hello
world
*/`},
},
},
out: []string{"// hello", "// world", "//"},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cg := makeComment(tc.in)
if cg == nil {
assert.Nil(t, tc.out)
return
}
var comments []string
for _, c := range cg.List {
comments = append(comments, c.Text)
}
assert.Equal(t, tc.out, comments)
})
}
}
func typeFromSource(t *testing.T, src string) types.Type {
fset := token.NewFileSet()
fullSrc := "package p\n\n" + src
f, err := parser.ParseFile(fset, "src.go", fullSrc, 0)
assert.NoError(t, err)
conf := types.Config{Importer: importer.Default()}
pkg, err := conf.Check("p", fset, []*goast.File{f}, nil)
assert.NoError(t, err)
obj := pkg.Scope().Lookup("T")
assert.NotNil(t, obj, "type T not found in source")
return obj.Type()
}
func TestSupportedType(t *testing.T) {
cases := []struct {
name string
src string
shouldError bool
errorContains string
}{
{name: "string", src: "type T string", shouldError: false},
{name: "pointer", src: "type T *string", shouldError: false},
{name: "slice", src: "type T []int", shouldError: false},
{name: "map", src: "type T map[string]bool", shouldError: false},
{name: "struct", src: "type T struct{ F string }", shouldError: false},
{name: "interface", src: "type T interface{}", shouldError: false},
{name: "recursive pointer", src: "type T *T", shouldError: true, errorContains: "recursive type"},
{name: "recursive struct field", src: "type T struct{ F *T }", shouldError: true, errorContains: "recursive type"},
{name: "map with non-string key", src: "type T map[int]string", shouldError: true, errorContains: "unsupported map key type"},
{name: "map with struct key", src: `type U struct{}
type T map[U]string`, shouldError: true, errorContains: "unsupported map key type"}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
typ := typeFromSource(t, tc.src)
err := supportedType(nil, typ)
if tc.shouldError {
assert.Error(t, err)
if tc.errorContains != "" {
assert.Contains(t, err.Error(), tc.errorContains)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestEnumField(t *testing.T) {
// Create a dummy generator. The actual fields of Generator are not used by enumField
// except for g.opts.types, which is empty here.
g := &Generator{}
defVal1 := "val1"
def1 := "1"
cases := []struct {
name string
typSrc string
opts *tagOptions
expectedErr bool
expectedCue cueast.Expr
}{
{
name: "string enum",
typSrc: "type T string",
opts: &tagOptions{Enum: []string{"val1", "val2"}},
expectedErr: true,
},
{
name: "int enum",
typSrc: "type T int",
opts: &tagOptions{Enum: []string{"1", "2"}},
expectedErr: true,
},
{
name: "string enum with default",
typSrc: "type T string",
opts: &tagOptions{Enum: []string{"val1", "val2"}, Default: &defVal1},
expectedErr: true,
},
{
name: "int enum with default",
typSrc: "type T int",
opts: &tagOptions{Enum: []string{"1", "2"}, Default: &def1},
expectedErr: true,
},
{
name: "unsupported type for enum",
typSrc: "type T struct{}",
opts: &tagOptions{Enum: []string{"val1"}},
expectedErr: true,
},
{
name: "invalid enum value for int type",
typSrc: "type T int",
opts: &tagOptions{Enum: []string{"not_an_int"}},
expectedErr: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
typ := typeFromSource(t, tc.typSrc)
expr, err := g.enumField(typ, tc.opts)
if tc.expectedErr {
assert.Error(t, err)
assert.Nil(t, expr)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedCue, expr)
}
})
}
}

View File

@ -18,12 +18,21 @@ package cuegen
import (
"io"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// errorWriter is an io.Writer that always returns an error.
type errorWriter struct{}
func (ew *errorWriter) Write(p []byte) (n int, err error) {
return 0, assert.AnError
}
// testGenerator is a helper function to create a valid Generator for tests.
func testGenerator(t *testing.T) *Generator {
g, err := NewGenerator("testdata/valid.go")
require.NoError(t, err)
@ -34,16 +43,47 @@ func testGenerator(t *testing.T) *Generator {
}
func TestNewGenerator(t *testing.T) {
g := testGenerator(t)
cases := []struct {
name string
path string
expectedErr bool
errContains string
}{
{
name: "valid package",
path: "testdata/valid.go",
expectedErr: false,
},
{
name: "non-existent package",
path: "testdata/non_existent.go",
expectedErr: true,
errContains: "could not load Go packages",
},
}
assert.NotNil(t, g.pkg)
assert.NotNil(t, g.types)
assert.Equal(t, g.opts.types, newDefaultOptions().types)
assert.Equal(t, g.opts.nullable, newDefaultOptions().nullable)
// assert can't compare function
assert.True(t, g.opts.typeFilter(nil))
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
g, err := NewGenerator(tc.path)
assert.Greater(t, len(g.types), 0)
if tc.expectedErr {
assert.Error(t, err)
assert.Nil(t, g)
if tc.errContains != "" {
assert.Contains(t, err.Error(), tc.errContains)
}
} else {
assert.NoError(t, err)
assert.NotNil(t, g)
assert.NotNil(t, g.pkg)
assert.NotNil(t, g.types)
assert.Equal(t, g.opts.types, newDefaultOptions().types)
assert.Equal(t, g.opts.nullable, newDefaultOptions().nullable)
assert.True(t, g.opts.typeFilter(nil))
assert.Greater(t, len(g.types), 0)
}
})
}
}
func TestGeneratorPackage(t *testing.T) {
@ -55,41 +95,180 @@ func TestGeneratorPackage(t *testing.T) {
func TestGeneratorGenerate(t *testing.T) {
g := testGenerator(t)
decls, err := g.Generate(WithTypes(map[string]Type{
"foo": TypeAny,
"bar": TypeAny,
}), nil)
assert.NoError(t, err)
assert.NotNil(t, decls)
cases := []struct {
name string
opts []Option
expectedErr bool
expectedLen int // Expected number of Decls
}{
{
name: "no options",
opts: nil,
expectedErr: false,
expectedLen: 26,
},
{
name: "with types option",
opts: []Option{WithTypes(map[string]Type{
"foo": TypeAny,
"bar": TypeAny,
})},
expectedErr: false,
expectedLen: 26,
},
}
decls, err = g.Generate()
assert.NoError(t, err)
assert.NotNil(t, decls)
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
decls, err := g.Generate(tc.opts...)
if tc.expectedErr {
assert.Error(t, err)
assert.Nil(t, decls)
} else {
assert.NoError(t, err)
assert.NotNil(t, decls)
assert.Len(t, decls, tc.expectedLen)
}
})
}
}
func TestGeneratorFormat(t *testing.T) {
g := testGenerator(t)
decls, err := g.Generate()
assert.NoError(t, err)
require.NoError(t, err)
require.NotNil(t, decls)
assert.NoError(t, g.Format(io.Discard, decls))
assert.NoError(t, g.Format(io.Discard, []Decl{nil, nil}))
assert.Error(t, g.Format(nil, decls))
assert.Error(t, g.Format(io.Discard, nil))
assert.Error(t, g.Format(io.Discard, []Decl{}))
cases := []struct {
name string
writer io.Writer
decls []Decl
expectedErr bool
errContains string
}{
{
name: "valid format",
writer: io.Discard,
decls: decls,
expectedErr: false,
},
{
name: "nil writer",
writer: nil,
decls: decls,
expectedErr: true,
errContains: "nil writer",
},
{
name: "empty decls",
writer: io.Discard,
decls: []Decl{},
expectedErr: true,
errContains: "invalid decls",
},
{
name: "writer error",
writer: &errorWriter{},
decls: decls,
expectedErr: true,
errContains: assert.AnError.Error(),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := g.Format(tc.writer, tc.decls)
if tc.expectedErr {
assert.Error(t, err)
if tc.errContains != "" {
assert.Contains(t, err.Error(), tc.errContains)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestLoadPackage(t *testing.T) {
pkg, err := loadPackage("testdata/valid.go")
cases := []struct {
name string
path string
expectedErr bool
errContains string
}{
{
name: "valid package",
path: "testdata/valid.go",
expectedErr: false,
},
{
name: "non-existent package",
path: "testdata/non_existent.go",
expectedErr: true,
errContains: "could not load Go packages",
},
{
name: "package with syntax error",
path: "testdata/invalid_syntax.go",
expectedErr: true,
errContains: "could not load Go packages",
},
}
// Create a temporary file with syntax errors for "package with syntax error" case
invalidGoContent := `package main
func main { // Missing parentheses
fmt.Println("Hello")
}`
err := os.WriteFile("testdata/invalid_syntax.go", []byte(invalidGoContent), 0644)
require.NoError(t, err)
require.NotNil(t, pkg)
require.Len(t, pkg.Errors, 0)
t.Cleanup(func() {
os.Remove("testdata/invalid_syntax.go")
})
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
pkg, err := loadPackage(tc.path)
if tc.expectedErr {
assert.Error(t, err)
assert.Nil(t, pkg)
if tc.errContains != "" {
assert.Contains(t, err.Error(), tc.errContains)
}
} else {
assert.NoError(t, err)
assert.NotNil(t, pkg)
assert.Len(t, pkg.Errors, 0)
}
})
}
}
func TestGetTypeInfo(t *testing.T) {
pkg, err := loadPackage("testdata/valid.go")
require.NoError(t, err)
cases := []struct {
name string
path string
expectedLen int
}{
{
name: "valid package",
path: "testdata/valid.go",
expectedLen: 40,
}}
require.Greater(t, len(getTypeInfo(pkg)), 0)
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
pkg, err := loadPackage(tc.path)
require.NoError(t, err)
typeInfo := getTypeInfo(pkg)
assert.Len(t, typeInfo, tc.expectedLen)
})
}
}

View File

@ -23,6 +23,7 @@ import (
"path/filepath"
"testing"
cueast "cuelang.org/go/cue/ast"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -30,51 +31,124 @@ import (
)
func TestGenerate(t *testing.T) {
got := bytes.Buffer{}
err := Generate(Options{
File: "testdata/valid.go",
Writer: &got,
Types: map[string]cuegen.Type{
"*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.Unstructured": cuegen.TypeEllipsis,
"*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.UnstructuredList": cuegen.TypeEllipsis,
},
Nullable: false,
})
require.NoError(t, err)
expected, err := os.ReadFile("testdata/valid.cue")
assert.NoError(t, err)
assert.NoError(t, err)
assert.Equal(t, string(expected), got.String())
}
func TestGenerateInvalid(t *testing.T) {
if err := filepath.Walk("testdata/invalid", func(path string, info os.FileInfo, e error) error {
if e != nil {
return e
}
if info.IsDir() {
return nil
}
t.Run("valid", func(t *testing.T) {
got := bytes.Buffer{}
err := Generate(Options{
File: path,
File: "testdata/valid.go",
Writer: &got,
Types: map[string]cuegen.Type{
"*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.Unstructured": cuegen.TypeEllipsis,
"*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.UnstructuredList": cuegen.TypeEllipsis,
},
Nullable: false,
})
require.NoError(t, err)
expected, err := os.ReadFile("testdata/valid.cue")
assert.NoError(t, err)
assert.Equal(t, string(expected), got.String())
})
t.Run("invalid", func(t *testing.T) {
if err := filepath.Walk("testdata/invalid", func(path string, info os.FileInfo, e error) error {
if e != nil {
return e
}
if info.IsDir() {
return nil
}
err := Generate(Options{
File: path,
Writer: io.Discard,
})
assert.Error(t, err)
return nil
}); err != nil {
t.Error(err)
}
})
t.Run("empty file", func(t *testing.T) {
err := Generate(Options{
File: "",
Writer: io.Discard,
})
assert.Error(t, err)
})
}
return nil
}); err != nil {
t.Error(err)
func TestExtractProviders(t *testing.T) {
t.Run("valid", func(t *testing.T) {
g, err := cuegen.NewGenerator("testdata/valid.go")
require.NoError(t, err)
providers, err := extractProviders(g.Package())
require.NoError(t, err)
require.Len(t, providers, 4)
assert.Equal(t, `"apply"`, providers[0].name)
assert.Equal(t, "ResourceParams", providers[0].params)
assert.Equal(t, "ResourceReturns", providers[0].returns)
assert.Equal(t, "Apply", providers[0].do)
})
t.Run("no provider map", func(t *testing.T) {
g, err := cuegen.NewGenerator("testdata/invalid/no_provider_map.go")
require.NoError(t, err)
_, err = extractProviders(g.Package())
assert.EqualError(t, err, "no provider function map found like 'map[string]github.com/kubevela/pkg/cue/cuex/runtime.ProviderFn'")
})
}
func TestModifyDecls(t *testing.T) {
tests := []struct {
name string
decls []cuegen.Decl
providers []provider
wantLen int
}{
{
name: "valid",
decls: []cuegen.Decl{
&cuegen.Struct{CommonFields: cuegen.CommonFields{Name: "Params", Expr: &cueast.StructLit{Elts: []cueast.Decl{&cueast.Field{Label: cueast.NewIdent("p"), Value: cueast.NewIdent("string")}}}}},
&cuegen.Struct{CommonFields: cuegen.CommonFields{Name: "Returns", Expr: &cueast.StructLit{Elts: []cueast.Decl{&cueast.Field{Label: cueast.NewIdent("r"), Value: cueast.NewIdent("string")}}}}},
},
providers: []provider{
{name: `"my-do"`, params: "Params", returns: "Returns", do: "MyDo"},
},
wantLen: 1,
},
{
name: "no providers",
decls: []cuegen.Decl{},
providers: []provider{},
wantLen: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
newDecls, err := modifyDecls("my-provider", tt.decls, tt.providers)
require.NoError(t, err)
require.Len(t, newDecls, tt.wantLen)
if tt.wantLen > 0 {
s, ok := newDecls[0].(*cuegen.Struct)
require.True(t, ok)
assert.Equal(t, "#MyDo", s.Name)
require.Len(t, s.Expr.(*cueast.StructLit).Elts, 4)
}
})
}
}
func TestGenerateEmptyError(t *testing.T) {
err := Generate(Options{
File: "",
Writer: io.Discard,
func TestRecoverAssert(t *testing.T) {
t.Run("panic recovery", func(t *testing.T) {
var err error
func() {
defer recoverAssert(&err, "test panic")
panic("panic message")
}()
assert.EqualError(t, err, "panic message: panic: test panic")
})
assert.Error(t, err)
}

View File

@ -0,0 +1,187 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package docgen
import (
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"sigs.k8s.io/yaml"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
)
func TestGenerateCUETemplateProperties(t *testing.T) {
// Read componentDef for a valid capability
componentDefYAML, err := os.ReadFile("testdata/componentDef.yaml")
require.NoError(t, err)
var componentDef v1beta1.ComponentDefinition
err = yaml.Unmarshal(componentDefYAML, &componentDef)
require.NoError(t, err)
// Define a struct to unmarshal the raw extension
type extensionSpec struct {
Template string `json:"template"`
}
var extSpec extensionSpec
err = yaml.Unmarshal(componentDef.Spec.Extension.Raw, &extSpec)
require.NoError(t, err)
// Define test cases
testCases := []struct {
name string
capability *types.Capability
expectedTables int
expectErr bool
}{
{
name: "valid component definition",
capability: &types.Capability{
Name: "test-component",
CueTemplate: extSpec.Template,
},
expectedTables: 2,
expectErr: false,
},
{
name: "invalid cue template",
capability: &types.Capability{
Name: "invalid-cue",
CueTemplate: `parameter: { image: }`,
},
expectErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ref := &ConsoleReference{}
doc, console, err := ref.GenerateCUETemplateProperties(tc.capability)
if tc.expectErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, doc)
require.Len(t, console, tc.expectedTables)
})
}
}
func TestGenerateTerraformCapabilityProperties(t *testing.T) {
ref := &ConsoleReference{}
type args struct {
cap types.Capability
}
type want struct {
tableName1 string
tableName2 string
errMsg string
}
testcases := map[string]struct {
args args
want want
}{
"normal": {
args: args{
cap: types.Capability{
TerraformConfiguration: `
resource "alicloud_oss_bucket" "bucket-acl" {
bucket = var.bucket
acl = var.acl
}
output "BUCKET_NAME" {
value = "${alicloud_oss_bucket.bucket-acl.bucket}.${alicloud_oss_bucket.bucket-acl.extranet_endpoint}"
}
variable "bucket" {
description = "OSS bucket name"
default = "vela-website"
type = string
}
variable "acl" {
description = "OSS bucket ACL, supported 'private', 'public-read', 'public-read-write'"
default = "private"
type = string
}
`,
},
},
want: want{
errMsg: "",
tableName1: "",
tableName2: "#### writeConnectionSecretToRef",
},
},
"configuration is in git remote": {
args: args{
cap: types.Capability{
Name: "ecs",
TerraformConfiguration: "https://github.com/wonderflow/terraform-alicloud-ecs-instance.git",
ConfigurationType: "remote",
},
},
want: want{
errMsg: "",
tableName1: "",
tableName2: "#### writeConnectionSecretToRef",
},
},
"configuration is not valid": {
args: args{
cap: types.Capability{
TerraformConfiguration: `abc`,
},
},
want: want{
errMsg: "failed to generate capability properties: :1,1-4: Argument or block definition required; An " +
"argument or block definition is required here. To set an argument, use the equals sign \"=\" to " +
"introduce the argument value.",
},
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
consoleRef, err := ref.GenerateTerraformCapabilityProperties(tc.args.cap)
var errMsg string
if err != nil {
errMsg = err.Error()
if diff := cmp.Diff(tc.want.errMsg, errMsg); diff != "" {
t.Errorf("\n%s\nGenerateTerraformCapabilityProperties(...): -want error, +got error:\n%s\n", name, diff)
}
} else {
if diff := cmp.Diff(2, len(consoleRef)); diff != "" {
t.Errorf("\n%s\nGenerateTerraformCapabilityProperties(...): -want, +got:\n%s\n", name, diff)
}
if diff := cmp.Diff(tc.want.tableName1, consoleRef[0].TableName); diff != "" {
t.Errorf("\n%s\nGenerateTerraformCapabilityProperties(...): -want, +got:\n%s\n", name, diff)
}
if diff := cmp.Diff(tc.want.tableName2, consoleRef[1].TableName); diff != "" {
t.Errorf("\n%s\nGenerateTerraformCapabilityProperties(...): -want, +got:\n%s\n", name, diff)
}
}
})
}
}

View File

@ -0,0 +1,186 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package docgen
import (
"testing"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/oam-dev/kubevela/apis/types"
)
func TestParseCapabilityFromUnstructured(t *testing.T) {
testCases := []struct {
name string
obj unstructured.Unstructured
wantCap types.Capability
wantErr bool
wantErrMsg string
}{
{
name: "trait definition",
obj: unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "core.oam.dev/v1beta1",
"kind": "TraitDefinition",
"metadata": map[string]interface{}{
"name": "my-trait",
},
"spec": map[string]interface{}{
"appliesToWorkloads": []interface{}{"webservice", "worker"},
"schematic": map[string]interface{}{
"cue": map[string]interface{}{
"template": "parameter: {}",
},
},
},
},
},
wantCap: types.Capability{
Name: "my-trait",
Type: types.TypeTrait,
AppliesTo: []string{"webservice", "worker"},
},
wantErr: false,
},
{
name: "component definition",
obj: unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "core.oam.dev/v1beta1",
"kind": "ComponentDefinition",
"metadata": map[string]interface{}{
"name": "my-comp",
},
"spec": map[string]interface{}{
"workload": map[string]interface{}{
"type": "worker",
},
"schematic": map[string]interface{}{
"cue": map[string]interface{}{
"template": "parameter: {}",
},
},
},
},
}, wantCap: types.Capability{
Name: "my-comp",
Type: types.TypeComponentDefinition,
},
wantErr: false,
},
{
name: "policy definition",
obj: unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "core.oam.dev/v1beta1",
"kind": "PolicyDefinition",
"metadata": map[string]interface{}{
"name": "my-policy",
},
"spec": map[string]interface{}{
"schematic": map[string]interface{}{
"cue": map[string]interface{}{
"template": "parameter: {}",
},
},
},
},
},
wantCap: types.Capability{
Name: "my-policy",
Type: types.TypePolicy,
},
wantErr: false,
},
{
name: "workflow step definition",
obj: unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "core.oam.dev/v1beta1",
"kind": "WorkflowStepDefinition",
"metadata": map[string]interface{}{
"name": "my-step",
},
"spec": map[string]interface{}{
"schematic": map[string]interface{}{
"cue": map[string]interface{}{
"template": "parameter: {}",
},
},
},
},
},
wantCap: types.Capability{
Name: "my-step",
Type: types.TypeWorkflowStep,
},
wantErr: false,
},
{
name: "unknown kind",
obj: unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "core.oam.dev/v1beta1",
"kind": "UnknownKind",
"metadata": map[string]interface{}{
"name": "my-unknown",
},
},
},
wantErr: true,
wantErrMsg: "unknown definition Type UnknownKind",
},
{
name: "malformed spec",
obj: unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "core.oam.dev/v1beta1",
"kind": "TraitDefinition",
"metadata": map[string]interface{}{
"name": "my-trait",
},
"spec": "this-should-be-a-map",
},
},
wantErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// The mapper is nil for these cases as they don't rely on it.
// A separate test would be needed for the mapper-dependent path.
cap, err := ParseCapabilityFromUnstructured(nil, tc.obj)
if tc.wantErr {
require.Error(t, err)
if tc.wantErrMsg != "" {
require.Contains(t, err.Error(), tc.wantErrMsg)
}
return
}
require.NoError(t, err)
require.Equal(t, tc.wantCap.Name, cap.Name)
require.Equal(t, tc.wantCap.Type, cap.Type)
require.Equal(t, tc.wantCap.AppliesTo, cap.AppliesTo)
})
}
}

View File

@ -21,34 +21,83 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestLoad(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, `{"Outputs":{"Chinese":"输出"}}`)
}))
defer svr.Close()
time.Sleep(time.Millisecond)
assert.Equal(t, En.Language(), Language("English"))
assert.Equal(t, En.Get("nihaoha"), "nihaoha")
assert.Equal(t, En.Get("AlibabaCloud"), "Alibaba Cloud")
var ni *I18n
assert.Equal(t, ni.Get("AlibabaCloud"), "Alibaba Cloud")
assert.Equal(t, ni.Get("AlibabaCloud."), "Alibaba Cloud")
assert.Equal(t, ni.Get("AlibabaCloud。"), "Alibaba Cloud")
assert.Equal(t, ni.Get("AlibabaCloud。 "), "Alibaba Cloud")
assert.Equal(t, ni.Get("AlibabaCloud 。 "), "Alibaba Cloud")
assert.Equal(t, ni.Get("AlibabaCloud \n "), "Alibaba Cloud")
assert.Equal(t, ni.Get(" A\n "), "A")
assert.Equal(t, ni.Get(" \n "), "")
func TestI18n(t *testing.T) {
t.Run("English defaults", func(t *testing.T) {
assert.Equal(t, En.Language(), Language("English"))
assert.Equal(t, En.Get("nihaoha"), "nihaoha")
assert.Equal(t, En.Get("AlibabaCloud"), "Alibaba Cloud")
})
assert.Equal(t, Zh.Language(), Language("Chinese"))
assert.Equal(t, Zh.Get("nihaoha"), "nihaoha")
assert.Equal(t, Zh.Get("AlibabaCloud"), "阿里云")
t.Run("Chinese defaults", func(t *testing.T) {
assert.Equal(t, Zh.Language(), Language("Chinese"))
assert.Equal(t, Zh.Get("nihaoha"), "nihaoha")
assert.Equal(t, Zh.Get("AlibabaCloud"), "阿里云")
})
LoadI18nData(svr.URL)
assert.Equal(t, Zh.Get("Outputs"), "输出")
t.Run("nil receiver", func(t *testing.T) {
var ni *I18n
assert.Equal(t, ni.Get("AlibabaCloud"), "Alibaba Cloud")
assert.Equal(t, ni.Get("AlibabaCloud."), "Alibaba Cloud")
assert.Equal(t, ni.Get("AlibabaCloud。"), "Alibaba Cloud")
assert.Equal(t, ni.Get("AlibabaCloud。 "), "Alibaba Cloud")
assert.Equal(t, ni.Get("AlibabaCloud 。 "), "Alibaba Cloud")
assert.Equal(t, ni.Get("AlibabaCloud \n "), "Alibaba Cloud")
assert.Equal(t, ni.Get(" A\n "), "A")
assert.Equal(t, ni.Get(" \n "), "")
})
t.Run("Get with fallback logic", func(t *testing.T) {
// Test suffix trimming
assert.Equal(t, "Description", En.Get("Description."))
assert.Equal(t, "描述", Zh.Get("描述。"))
// Test lowercase fallback (Note: this reveals a bug, as it doesn't find the capitalized key)
assert.Equal(t, "description", En.Get("description"))
assert.Equal(t, "description", Zh.Get("description"))
})
}
func TestLoadI18nData(t *testing.T) {
t.Run("Load external data", func(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, `{"Outputs":{"Chinese":"输出"}}`)
}))
defer svr.Close()
LoadI18nData(svr.URL)
assert.Equal(t, "输出", Zh.Get("Outputs"))
})
}
func TestLoadI18nDataErrors(t *testing.T) {
t.Run("http error", func(t *testing.T) {
// Check that a non-existent key is not translated before the call
assert.Equal(t, "TestKey", Zh.Get("TestKey"))
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer svr.Close()
LoadI18nData(svr.URL)
// Assert that the key is still not translated
assert.Equal(t, "TestKey", Zh.Get("TestKey"))
})
t.Run("malformed json", func(t *testing.T) {
// Check that another non-existent key is not translated
assert.Equal(t, "AnotherKey", Zh.Get("AnotherKey"))
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprint(w, `this-is-not-json`)
}))
defer svr.Close()
LoadI18nData(svr.URL)
// Assert that the key is still not translated
assert.Equal(t, "AnotherKey", Zh.Get("AnotherKey"))
})
}

View File

@ -0,0 +1,126 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package docgen
import (
"sort"
"strings"
"testing"
"github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
)
func TestGenerateConsoleDocument(t *testing.T) {
testCases := []struct {
name string
title string
schema *openapi3.Schema
wantOutput string
wantErr bool
}{
{
name: "empty schema",
title: "Test",
schema: &openapi3.Schema{},
wantOutput: "",
},
{
name: "simple schema",
title: "",
schema: &openapi3.Schema{
Properties: map[string]*openapi3.SchemaRef{
"name": {
Value: &openapi3.Schema{
Title: "name",
Description: "The name of the resource.",
Type: &openapi3.Types{openapi3.TypeString},
},
},
"port": {
Value: &openapi3.Schema{
Title: "port",
Description: "The port to expose.",
Type: &openapi3.Types{openapi3.TypeInteger},
},
},
},
},
wantOutput: `
+------+---------+---------------------------+----------+---------+---------+
| NAME | TYPE | DESCRIPTION | REQUIRED | OPTIONS | DEFAULT |
+------+---------+---------------------------+----------+---------+---------+
| name | string | The name of the resource. | false | | |
| port | integer | The port to expose. | false | | |
+------+---------+---------------------------+----------+---------+---------+
`,
},
{
name: "nested schema",
title: "parent",
schema: &openapi3.Schema{
Required: []string{"child"},
Properties: map[string]*openapi3.SchemaRef{
"child": {
Value: &openapi3.Schema{
Title: "child",
Type: &openapi3.Types{openapi3.TypeObject},
Properties: map[string]*openapi3.SchemaRef{
"leaf": {
Value: &openapi3.Schema{
Title: "leaf",
Type: &openapi3.Types{openapi3.TypeString},
},
},
},
},
},
},
},
wantOutput: `parent
+----------------+--------+-------------+----------+---------+---------+
| NAME | TYPE | DESCRIPTION | REQUIRED | OPTIONS | DEFAULT |
+----------------+--------+-------------+----------+---------+---------+
| (parent).child | object | | true | | |
+----------------+--------+-------------+----------+---------+---------+
parent.child
+---------------------+--------+-------------+----------+---------+---------+
| NAME | TYPE | DESCRIPTION | REQUIRED | OPTIONS | DEFAULT |
+---------------------+--------+-------------+----------+---------+---------+
| (parent.child).leaf | string | | false | | |
+---------------------+--------+-------------+----------+---------+---------+
`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
doc, err := GenerateConsoleDocument(tc.title, tc.schema)
if tc.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
// Trim whitespace for consistent comparison and sort lines to avoid flakiness
expectedLines := strings.Split(strings.TrimSpace(tc.wantOutput), "\n")
actualLines := strings.Split(strings.TrimSpace(doc), "\n")
sort.Strings(expectedLines)
sort.Strings(actualLines)
require.Equal(t, expectedLines, actualLines)
})
}
}

View File

@ -24,7 +24,6 @@ import (
"reflect"
"testing"
"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/getkin/kin-openapi/openapi3"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
@ -262,103 +261,6 @@ func TestWalkParameterSchema(t *testing.T) {
}
}
func TestGenerateTerraformCapabilityProperties(t *testing.T) {
ref := &ConsoleReference{}
type args struct {
cap types.Capability
}
type want struct {
tableName1 string
tableName2 string
errMsg string
}
testcases := map[string]struct {
args args
want want
}{
"normal": {
args: args{
cap: types.Capability{
TerraformConfiguration: `
resource "alicloud_oss_bucket" "bucket-acl" {
bucket = var.bucket
acl = var.acl
}
output "BUCKET_NAME" {
value = "${alicloud_oss_bucket.bucket-acl.bucket}.${alicloud_oss_bucket.bucket-acl.extranet_endpoint}"
}
variable "bucket" {
description = "OSS bucket name"
default = "vela-website"
type = string
}
variable "acl" {
description = "OSS bucket ACL, supported 'private', 'public-read', 'public-read-write'"
default = "private"
type = string
}
`,
},
},
want: want{
errMsg: "",
tableName1: "",
tableName2: "#### writeConnectionSecretToRef",
},
},
"configuration is in git remote": {
args: args{
cap: types.Capability{
Name: "ecs",
TerraformConfiguration: "https://github.com/wonderflow/terraform-alicloud-ecs-instance.git",
ConfigurationType: "remote",
},
},
want: want{
errMsg: "",
tableName1: "",
tableName2: "#### writeConnectionSecretToRef",
},
},
"configuration is not valid": {
args: args{
cap: types.Capability{
TerraformConfiguration: `abc`,
},
},
want: want{
errMsg: "failed to generate capability properties: :1,1-4: Argument or block definition required; An " +
"argument or block definition is required here. To set an argument, use the equals sign \"=\" to " +
"introduce the argument value.",
},
},
}
for name, tc := range testcases {
consoleRef, err := ref.GenerateTerraformCapabilityProperties(tc.args.cap)
var errMsg string
if err != nil {
errMsg = err.Error()
if diff := cmp.Diff(tc.want.errMsg, errMsg, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nGenerateTerraformCapabilityProperties(...): -want error, +got error:\n%s\n", name, diff)
}
} else {
if diff := cmp.Diff(len(consoleRef), 2); diff != "" {
t.Errorf("\n%s\nGenerateTerraformCapabilityProperties(...): -want, +got:\n%s\n", name, diff)
}
if diff := cmp.Diff(tc.want.tableName1, consoleRef[0].TableName); diff != "" {
t.Errorf("\n%s\nGenerateTerraformCapabilityProperties(...): -want, +got:\n%s\n", name, diff)
}
if diff := cmp.Diff(tc.want.tableName2, consoleRef[1].TableName); diff != "" {
t.Errorf("\n%s\nGexnerateTerraformCapabilityProperties(...): -want, +got:\n%s\n", name, diff)
}
}
}
}
func TestPrepareTerraformOutputs(t *testing.T) {
type args struct {
tableName string