From 439dd3a24dd24f9347715984534b1f6f703607d6 Mon Sep 17 00:00:00 2001 From: "jose.vazquez" Date: Tue, 14 May 2024 17:40:43 +0200 Subject: [PATCH] [skunkworks] Auditing contract testing Signed-off-by: jose.vazquez --- .github/workflows/cloud-tests.yml | 5 + .github/workflows/test-contract.yml | 37 +++ .github/workflows/test.yml | 5 + Makefile | 5 + internal/translayer/auditing/auditing.go | 47 ++++ internal/translayer/auditing/translate.go | 56 ++++ internal/translayer/client.go | 20 ++ test/contract/auditing/auditing_test.go | 144 ++++++++++ test/contract/auditing/test.yml | 6 + test/contract/contract.go | 26 ++ test/e2e/e2e_suite_test.go | 4 +- test/helper/control/enable.go | 7 + test/helper/launcher/credentials.go | 15 + test/helper/launcher/launcher.go | 257 ++++++++++++++++++ test/helper/launcher/launcher_test.go | 35 +++ test/helper/launcher/run.go | 14 + test/helper/launcher/test.yml | 6 + test/helper/launcher/wait.go | 28 ++ .../int/clusterwide/integration_suite_test.go | 5 +- test/int/integration_suite_test.go | 5 +- 20 files changed, 716 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/test-contract.yml create mode 100644 internal/translayer/auditing/auditing.go create mode 100644 internal/translayer/auditing/translate.go create mode 100644 internal/translayer/client.go create mode 100644 test/contract/auditing/auditing_test.go create mode 100644 test/contract/auditing/test.yml create mode 100644 test/contract/contract.go create mode 100644 test/helper/launcher/credentials.go create mode 100644 test/helper/launcher/launcher.go create mode 100644 test/helper/launcher/launcher_test.go create mode 100644 test/helper/launcher/run.go create mode 100644 test/helper/launcher/test.yml create mode 100644 test/helper/launcher/wait.go diff --git a/.github/workflows/cloud-tests.yml b/.github/workflows/cloud-tests.yml index 8ec5469c63..6fb9bbbe6a 100644 --- a/.github/workflows/cloud-tests.yml +++ b/.github/workflows/cloud-tests.yml @@ -54,6 +54,11 @@ jobs: with: forked: ${{ inputs.forked }} + contract-tests: + needs: allowed + uses: ./.github/workflows/test-contract.yml + secrets: inherit + e2e-tests: needs: allowed uses: ./.github/workflows/test-e2e.yml diff --git a/.github/workflows/test-contract.yml b/.github/workflows/test-contract.yml new file mode 100644 index 0000000000..8cd8d30896 --- /dev/null +++ b/.github/workflows/test-contract.yml @@ -0,0 +1,37 @@ +name: Contract Tests + +on: + workflow_call: + workflow_dispatch: + +jobs: + contract: + name: Contract Tests + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + ref: ${{github.event.pull_request.head.sha}} + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: "${{ github.workspace }}/go.mod" + cache: false + + - name: Create k8s Kind Cluster + uses: helm/kind-action@v1.10.0 + with: + version: v0.22.0 + config: test/helper/e2e/config/kind.yaml + node_image: kindest/node:v1.29.2 + + - name: Run Contract Testing + env: + AKO_CONTRACT_TEST: 1 + MCLI_OPS_MANAGER_URL: https://cloud-qa.mongodb.com + MCLI_ORG_ID: ${{ secrets.ATLAS_ORG_ID }} + MCLI_PUBLIC_API_KEY: ${{ secrets.ATLAS_PUBLIC_KEY }} + MCLI_PRIVATE_API_KEY: ${{ secrets.ATLAS_PRIVATE_KEY }} + run: make contract-tests diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 951462d216..8180fbdad9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,3 +72,8 @@ jobs: secrets: inherit with: forked: ${{ github.event.repository.full_name != github.event.pull_request.head.repo.full_name }} + + contract-tests: # TODO: remove shortcut + needs: run-tests + uses: ./.github/workflows/test-contract.yml + secrets: inherit diff --git a/Makefile b/Makefile index 7c5d3dfd78..80ed019c52 100644 --- a/Makefile +++ b/Makefile @@ -498,3 +498,8 @@ gen-sdlc-checklist: envsubst docker-sbom ## Generate the SDLC checklist .PHONY: clear-e2e-leftovers clear-e2e-leftovers: ## Clear the e2e test leftovers quickly git restore bundle* config deploy + +.PHONY: contract-tests +contract-tests: ## Run contract tests + go clean -testcache + AKO_CONTRACT_TEST=1 go test -race -cover ./test/contract/... diff --git a/internal/translayer/auditing/auditing.go b/internal/translayer/auditing/auditing.go new file mode 100644 index 0000000000..a944384365 --- /dev/null +++ b/internal/translayer/auditing/auditing.go @@ -0,0 +1,47 @@ +package auditing + +import ( + "context" + "fmt" + + "go.mongodb.org/atlas-sdk/v20231115008/admin" + "go.uber.org/zap" + "k8s.io/apimachinery/pkg/types" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translayer" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas" +) + +type Service interface { + Get(ctx context.Context, projectID string) (*Auditing, error) + Set(ctx context.Context, projectID string, auditing *Auditing) error +} + +type service struct { + admin.AuditingApi +} + +func NewService(ctx context.Context, provider atlas.Provider, secretRef *types.NamespacedName, log *zap.SugaredLogger) (Service, error) { + client, err := translayer.NewVersionedClient(ctx, provider, secretRef, log) + if err != nil { + return nil, err + } + return NewFromAuditingAPI(client.AuditingApi), nil +} + +func NewFromAuditingAPI(api admin.AuditingApi) *service { + return &service{AuditingApi: api} +} + +func (s *service) Get(ctx context.Context, projectID string) (*Auditing, error) { + auditLog, _, err := s.AuditingApi.GetAuditingConfiguration(ctx, projectID).Execute() + if err != nil { + return nil, fmt.Errorf("failed to get audit log from Atlas: %w", err) + } + return fromAtlas(auditLog) +} + +func (s *service) Set(ctx context.Context, projectID string, auditing *Auditing) error { + _, _, err := s.AuditingApi.UpdateAuditingConfiguration(ctx, projectID, toAtlas(auditing)).Execute() + return err +} diff --git a/internal/translayer/auditing/translate.go b/internal/translayer/auditing/translate.go new file mode 100644 index 0000000000..045e21ebed --- /dev/null +++ b/internal/translayer/auditing/translate.go @@ -0,0 +1,56 @@ +package auditing + +import ( + "fmt" + + "go.mongodb.org/atlas-sdk/v20231115008/admin" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" +) + +type AuditingConfigType string + +const ( + None AuditingConfigType = "NONE" + FilterBuilder AuditingConfigType = "FILTER_BUILDER" + FilterJSON AuditingConfigType = "FILTER_JSON" +) + +type Auditing struct { + Enabled bool + AuditAuthorizationSuccess bool + ConfigurationType AuditingConfigType + AuditFilter string +} + +func toAtlas(auditing *Auditing) *admin.AuditLog { + return &admin.AuditLog{ + Enabled: pointer.MakePtr(auditing.Enabled), + AuditAuthorizationSuccess: pointer.MakePtr(auditing.AuditAuthorizationSuccess), + AuditFilter: pointer.MakePtr(auditing.AuditFilter), + // ConfigurationType is not set on the PATCH operation to Atlas + } +} + +func fromAtlas(auditLog *admin.AuditLog) (*Auditing, error) { + cfgType, err := configTypeFromAtlas(auditLog.ConfigurationType) + if err != nil { + return nil, err + } + return &Auditing{ + Enabled: pointer.GetOrDefault(auditLog.Enabled, false), + AuditAuthorizationSuccess: pointer.GetOrDefault(auditLog.AuditAuthorizationSuccess, false), + ConfigurationType: cfgType, + AuditFilter: pointer.GetOrDefault(auditLog.AuditFilter, ""), + }, nil +} + +func configTypeFromAtlas(configType *string) (AuditingConfigType, error) { + ct := pointer.GetOrDefault(configType, string(None)) + switch ct { + case string(None), string(FilterBuilder), string(FilterJSON): + return AuditingConfigType(ct), nil + default: + return AuditingConfigType(ct), fmt.Errorf("unsupported Auditing Config type %q", ct) + } +} diff --git a/internal/translayer/client.go b/internal/translayer/client.go new file mode 100644 index 0000000000..6d7fd59db8 --- /dev/null +++ b/internal/translayer/client.go @@ -0,0 +1,20 @@ +package translayer + +import ( + "context" + "fmt" + + "go.mongodb.org/atlas-sdk/v20231115008/admin" + "go.uber.org/zap" + "k8s.io/apimachinery/pkg/types" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas" +) + +func NewVersionedClient(ctx context.Context, provider atlas.Provider, secretRef *types.NamespacedName, log *zap.SugaredLogger) (*admin.APIClient, error) { + apiClient, _, err := provider.SdkClient(ctx, secretRef, log) + if err != nil { + return nil, fmt.Errorf("failed to instantiate Versioned Atlas client: %w", err) + } + return apiClient, nil +} diff --git a/test/contract/auditing/auditing_test.go b/test/contract/auditing/auditing_test.go new file mode 100644 index 0000000000..ee986fd32a --- /dev/null +++ b/test/contract/auditing/auditing_test.go @@ -0,0 +1,144 @@ +package auditing + +import ( + "context" + _ "embed" + "log" + "testing" + "time" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translayer/auditing" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/control" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/launcher" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/contract" +) + +//go:embed test.yml +var testYml string + +const ( + testVersion = "2.1.0" +) + +func TestMain(m *testing.M) { + if !control.Enabled("AKO_CONTRACT_TEST") { + log.Printf("Skipping contract test as AKO_CONTRACT_TEST is unset") + return + } + l := launcher.NewFromEnv(testVersion) + l.Launch( + testYml, + launcher.WaitReady("atlasprojects/my-project", 30*time.Second)) + if !control.Enabled("SKIP_CLEANUP") { // allow to reuse Atlas resources for local tests + defer l.Cleanup() + } + m.Run() +} + +func TestDefaultAuditingGet(t *testing.T) { + testProjectID := mustReadProjectID() + ctx := context.Background() + as := auditing.NewFromAuditingAPI(contract.MustVersionedClient(t, ctx).AuditingApi) + + result, err := as.Get(ctx, testProjectID) + + require.NoError(t, err) + result.ConfigurationType = "" // Do not expect the returned cfg type to match + if result.AuditFilter == "{}" { + // Support re-runs, as we cannot get the filter back to empty + result.AuditFilter = "" + } + assert.Equal(t, defaultAtlasAuditing(), result) +} + +func defaultAtlasAuditing() *auditing.Auditing { + return &auditing.Auditing{ + Enabled: false, + AuditAuthorizationSuccess: false, + AuditFilter: "", + } +} + +func TestSyncs(t *testing.T) { + testCases := []struct { + title string + auditing *auditing.Auditing + }{ + { + title: "Just enabled", + auditing: &auditing.Auditing{ + Enabled: true, + AuditAuthorizationSuccess: false, + AuditFilter: "{}", // must sent empty JSON to overwrite previous state + }, + }, + { + title: "Auth success logs as well", + auditing: &auditing.Auditing{ + Enabled: true, + AuditAuthorizationSuccess: true, + AuditFilter: "{}", + }, + }, + { + title: "With a filter", + auditing: &auditing.Auditing{ + Enabled: true, + AuditAuthorizationSuccess: false, + AuditFilter: `{"atype":"authenticate"}`, + }, + }, + { + title: "With a filter and success logs", + auditing: &auditing.Auditing{ + Enabled: true, + AuditAuthorizationSuccess: true, + AuditFilter: `{"atype":"authenticate"}`, + }, + }, + { + title: "All set but disabled", + auditing: &auditing.Auditing{ + Enabled: false, + AuditAuthorizationSuccess: true, + AuditFilter: `{"atype":"authenticate"}`, + }, + }, + { + title: "Default (disabled) case", + auditing: &auditing.Auditing{ + Enabled: false, + AuditAuthorizationSuccess: false, + AuditFilter: "{}", + }, + }, + } + testProjectID := mustReadProjectID() + ctx := context.Background() + as := auditing.NewFromAuditingAPI(contract.MustVersionedClient(t, ctx).AuditingApi) + + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + err := as.Set(ctx, testProjectID, tc.auditing) + require.NoError(t, err) + + result, err := as.Get(ctx, testProjectID) + require.NoError(t, err) + result.ConfigurationType = "" // Do not expect the returned cfg type to match + assert.Equal(t, tc.auditing, result) + }) + } +} + +func mustReadProjectID() string { + l := launcher.NewFromEnv(testVersion) + output, err := l.Kubectl("get", "atlasprojects/my-project", "-o=jsonpath={.status.id}") + if err != nil { + log.Fatalf("Failed to get test project id: %v", err) + } + return output +} diff --git a/test/contract/auditing/test.yml b/test/contract/auditing/test.yml new file mode 100644 index 0000000000..f1a6489a0b --- /dev/null +++ b/test/contract/auditing/test.yml @@ -0,0 +1,6 @@ +apiVersion: atlas.mongodb.com/v1 +kind: AtlasProject +metadata: + name: my-project +spec: + name: Test Atlas Operator Project diff --git a/test/contract/contract.go b/test/contract/contract.go new file mode 100644 index 0000000000..57e49980a0 --- /dev/null +++ b/test/contract/contract.go @@ -0,0 +1,26 @@ +package contract + +import ( + "context" + "os" + "testing" + + "go.mongodb.org/atlas-sdk/v20231115008/admin" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas" +) + +func NewVersionedClient(ctx context.Context) (*admin.APIClient, error) { + domain := os.Getenv("MCLI_OPS_MANAGER_URL") + pubKey := os.Getenv("MCLI_PUBLIC_API_KEY") + prvKey := os.Getenv("MCLI_PRIVATE_API_KEY") + return atlas.NewClient(domain, pubKey, prvKey) +} + +func MustVersionedClient(t *testing.T, ctx context.Context) *admin.APIClient { + client, err := NewVersionedClient(ctx) + if err != nil { + t.Fatalf("Failed to get Atlas versioned client: %v", err) + } + return client +} diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 5d0fade281..75c875b314 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -26,9 +26,7 @@ var ( ) func TestE2e(t *testing.T) { - if !control.Enabled("AKO_E2E_TEST") { - t.Skip("Skipping e2e tests, AKO_E2E_TEST is not set") - } + control.SkipTestUnless(t, "AKO_E2E_TEST") RegisterFailHandler(Fail) RunSpecs(t, "Atlas Operator E2E Test Suite") diff --git a/test/helper/control/enable.go b/test/helper/control/enable.go index e75bf673aa..cef919bea5 100644 --- a/test/helper/control/enable.go +++ b/test/helper/control/enable.go @@ -3,9 +3,16 @@ package control import ( "os" "strings" + "testing" ) func Enabled(envvar string) bool { value := strings.ToLower(os.Getenv(envvar)) return value == "1" } + +func SkipTestUnless(t *testing.T, envvar string) { + if !Enabled(envvar) { + t.Skipf("Skipping tests, %s is not set", envvar) + } +} diff --git a/test/helper/launcher/credentials.go b/test/helper/launcher/credentials.go new file mode 100644 index 0000000000..3e73511e1b --- /dev/null +++ b/test/helper/launcher/credentials.go @@ -0,0 +1,15 @@ +package launcher + +type AtlasCredentials struct { + OrgID string + PublicKey string + PrivateKey string +} + +func credentialsFromEnv() AtlasCredentials { + return AtlasCredentials{ + OrgID: MustSetEnv("MCLI_ORG_ID"), + PublicKey: MustSetEnv("MCLI_PUBLIC_API_KEY"), + PrivateKey: MustSetEnv("MCLI_PRIVATE_API_KEY"), + } +} diff --git a/test/helper/launcher/launcher.go b/test/helper/launcher/launcher.go new file mode 100644 index 0000000000..b900fed46e --- /dev/null +++ b/test/helper/launcher/launcher.go @@ -0,0 +1,257 @@ +package launcher + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "path" + "strings" +) + +const ( + ExpectedContext = "kind-kind" + + LauncherTestInstall = "test-ako" + + HelmRepoURL = "https://mongodb.github.io/helm-charts" + + RepoRef = "mongodb" + + OperatorChart = "mongodb-atlas-operator" + + AtlasURI = "https://cloud-qa.mongodb.com" + + // #nosec G101 -- This is just a name + AtlasSecretName = "mongodb-atlas-operator-api-key" +) + +type Launcher struct { + credentials AtlasCredentials + version string + cmdOutput bool + clearKind bool + clearOperator bool + clearSecret bool + appliedYAMLs []string +} + +// NewLauncher creates a new operator Launcher +func NewLauncher(creds AtlasCredentials, version string, cmdOutput bool) *Launcher { + return &Launcher{credentials: creds, version: version, cmdOutput: cmdOutput, appliedYAMLs: []string{}} +} + +// NewFromEnv creates an operator Launcher using defaults and credentials from env vars +func NewFromEnv(version string) *Launcher { + return NewLauncher(credentialsFromEnv(), version, true) +} + +// MustSetEnv sets the env var value given, or panics if the env var is not set +func MustSetEnv(envvar string) string { + value, ok := os.LookupEnv(envvar) + if !ok { + panic(fmt.Errorf("environment variable %s not set", envvar)) + } + return value +} + +// Launch will try to launch the operator and apply the given YAML for it to handle +func (l *Launcher) Launch(yml string, waitCfg *WaitConfig) error { + if err := l.ensureK8sCluster(); err != nil { + return err + } + if err := l.ensureOperator(); err != nil { + return err + } + if err := l.ensureAtlasSecret(); err != nil { + return err + } + if err := l.kubeApply(yml); err != nil { + return err + } + l.appliedYAMLs = append(l.appliedYAMLs, yml) + return l.kubeWait(waitCfg) +} + +func (l *Launcher) Kubectl(args ...string) (string, error) { + return l.run("kubectl", args...) +} + +// Cleanup related Launcher test resources, merely wipe the kind cluster +func (l *Launcher) Cleanup() error { + for _, yml := range l.appliedYAMLs { + l.kubeDelete(yml) + } + if l.clearOperator { + if err := l.uninstall(); err != nil { + return err + } + } + if l.clearSecret { + if err := l.kubeDeleteAtlasSecret(); err != nil { + return err + } + } + if l.clearKind { + return l.stopKind() + } + return nil +} + +func (l *Launcher) ensureK8sCluster() error { + if !l.isKubeConfigAvailable() { + return l.startKind() + } + return nil +} + +func (l *Launcher) isKubeConfigAvailable() bool { + out, err := l.run("kubectl", "config", "current-context") + return err == nil && strings.Contains(out, ExpectedContext) +} + +func (l *Launcher) startKind() error { + err := l.silentRun("kind", "create", "cluster") + if err == nil { + l.clearKind = true + } + return err +} + +func (l *Launcher) stopKind() error { + return l.silentRun("kind", "delete", "cluster") +} + +func (l *Launcher) ensureOperator() error { + if !l.isInstalled() { + return l.install() + } + return nil +} + +func (l *Launcher) isInstalled() bool { + result, err := l.run("helm", "ls", "-a", "-A") + if err != nil || !strings.Contains(result, LauncherTestInstall) { + return false + } + scanner := bufio.NewScanner(strings.NewReader(result)) + for scanner.Scan() { + str := scanner.Text() + if strings.Contains(str, LauncherTestInstall) { + parts := strings.Split(str, "\t") + installedVersion := strings.TrimSpace(parts[len(parts)-1]) + return l.version == installedVersion + } + } + return false +} + +func (l *Launcher) install() error { + err := l.silentRun("helm", "repo", "add", RepoRef, HelmRepoURL) + update := false + if err != nil { + if strings.Contains(err.Error(), "already exists") { + update = true + } else { + return fmt.Errorf("failed to set up mongodb helm chart repo: %w", err) + } + } + if update { + if err := l.silentRun("helm", "repo", "update", RepoRef); err != nil { + return fmt.Errorf("failed to update mongodb helm chart repo: %w", err) + } + } + if err != nil && !strings.Contains(err.Error(), "already exists") { + return fmt.Errorf("failed to set up mongodb helm chart repo: %w", err) + } + err = l.silentRun("helm", "install", LauncherTestInstall, path.Join(RepoRef, OperatorChart), + "--version", l.version, "--atomic", "--set", fmt.Sprintf("atlasURI=%s", AtlasURI)) + if err == nil { + l.clearOperator = true + } + return err +} + +func (l *Launcher) uninstall() error { + return l.silentRun("helm", "uninstall", LauncherTestInstall) +} + +func (l *Launcher) ensureAtlasSecret() error { + if !l.atlasSecretAvailable() { + l.clearSecret = true + return l.kubeCreateAtlasSecret() + } + return nil +} + +func (l *Launcher) atlasSecretAvailable() bool { + return l.silentRun("kubectl", "get", "secret", AtlasSecretName) == nil +} + +func (l *Launcher) kubeCreateAtlasSecret() error { + if err := l.silentRun("kubectl", "create", "secret", "generic", AtlasSecretName, + fmt.Sprintf("--from-literal=orgId=%s", l.credentials.OrgID), + fmt.Sprintf("--from-literal=publicApiKey=%s", l.credentials.PublicKey), + fmt.Sprintf("--from-literal=privateApiKey=%s", l.credentials.PrivateKey)); err != nil { + return err + } + return l.silentRun("kubectl", "label", "secret", AtlasSecretName, "atlas.mongodb.com/type=credentials") +} + +func (l *Launcher) kubeDeleteAtlasSecret() error { + return l.silentRun("kubectl", "delete", "secret", AtlasSecretName) +} + +func (l *Launcher) kubeApply(yml string) error { + return l.pipeRun(yml, "kubectl", "apply", "-f", "-") +} + +func (l *Launcher) kubeDelete(yml string) error { + return l.pipeRun(yml, "kubectl", "delete", "-f", "-") +} + +func (l *Launcher) kubeWait(cfg *WaitConfig) error { + if cfg == NoWait { + return nil + } + return l.silentRun("kubectl", cfg.waitArgs()...) +} + +func (l *Launcher) run(cmd string, args ...string) (string, error) { + buf := bytes.NewBufferString("") + err := run(nil, l.stdout(buf), l.stderr(buf), cmd, args...) + return buf.String(), err +} + +func (l *Launcher) silentRun(cmd string, args ...string) error { + return silenceReturn(l.run(cmd, args...)) +} + +func (l *Launcher) pipeRun(stdin, cmd string, args ...string) error { + buf := bytes.NewBufferString("") + input := bytes.NewBufferString(stdin) + err := run(input, l.stdout(buf), l.stderr(buf), cmd, args...) + return silenceReturn(buf.String(), err) +} + +func (l *Launcher) stdout(w io.Writer) io.Writer { + if l.cmdOutput { + return io.MultiWriter(w, os.Stdout) + } + return w +} + +func (l *Launcher) stderr(w io.Writer) io.Writer { + if l.cmdOutput { + return io.MultiWriter(w, os.Stderr) + } + return w +} + +func silenceReturn(msg string, err error) error { + if err != nil { + return fmt.Errorf("%s: %w", msg, err) + } + return nil +} diff --git a/test/helper/launcher/launcher_test.go b/test/helper/launcher/launcher_test.go new file mode 100644 index 0000000000..e2a79b5d8b --- /dev/null +++ b/test/helper/launcher/launcher_test.go @@ -0,0 +1,35 @@ +package launcher_test + +import ( + "testing" + "time" + + _ "embed" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/control" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/launcher" +) + +//go:embed test.yml +var testYml string + +const ( + testVersion = "2.1.0" +) + +func TestLaunch(t *testing.T) { + control.SkipTestUnless(t, "AKO_LAUNCHER_TEST") + + l := launcher.NewFromEnv(testVersion) + assert.NoError(t, l.Launch( + testYml, + launcher.WaitReady("atlasprojects/my-project", 30*time.Second))) + // retest should also work and faster (no need to re-create resources) + assert.NoError(t, l.Launch( + testYml, + launcher.WaitReady("atlasprojects/my-project", 30*time.Second))) + require.NoError(t, l.Cleanup()) +} diff --git a/test/helper/launcher/run.go b/test/helper/launcher/run.go new file mode 100644 index 0000000000..e0cc091d52 --- /dev/null +++ b/test/helper/launcher/run.go @@ -0,0 +1,14 @@ +package launcher + +import ( + "io" + "os/exec" +) + +func run(stdin io.Reader, stdout, stderr io.Writer, cmd string, args ...string) error { + c := exec.Command(cmd, args...) + c.Stdin = stdin + c.Stdout = stdout + c.Stderr = stderr + return c.Run() +} diff --git a/test/helper/launcher/test.yml b/test/helper/launcher/test.yml new file mode 100644 index 0000000000..f1a6489a0b --- /dev/null +++ b/test/helper/launcher/test.yml @@ -0,0 +1,6 @@ +apiVersion: atlas.mongodb.com/v1 +kind: AtlasProject +metadata: + name: my-project +spec: + name: Test Atlas Operator Project diff --git a/test/helper/launcher/wait.go b/test/helper/launcher/wait.go new file mode 100644 index 0000000000..87d7c0ba62 --- /dev/null +++ b/test/helper/launcher/wait.go @@ -0,0 +1,28 @@ +package launcher + +import ( + "fmt" + "time" +) + +var ( + NoWait = (*WaitConfig)(nil) +) + +type WaitConfig struct { + Condition string + Target string + Timeout time.Duration +} + +func WaitReady(target string, timeout time.Duration) *WaitConfig { + return &WaitConfig{Condition: "condition=Ready", Target: target, Timeout: timeout} +} + +func (cfg *WaitConfig) waitArgs() []string { + args := []string{"wait", fmt.Sprintf("--for=%s", cfg.Condition), cfg.Target} + if cfg.Timeout != 0 { + args = append(args, fmt.Sprintf("--timeout=%v", cfg.Timeout)) + } + return args +} diff --git a/test/int/clusterwide/integration_suite_test.go b/test/int/clusterwide/integration_suite_test.go index 1d0c8f7460..fb74afd2ab 100644 --- a/test/int/clusterwide/integration_suite_test.go +++ b/test/int/clusterwide/integration_suite_test.go @@ -68,9 +68,7 @@ const ( ) func TestAPIs(t *testing.T) { - if !control.Enabled("AKO_INT_TEST") { - t.Skip("Skipping int tests, AKO_INT_TEST is not set") - } + control.SkipTestUnless(t, "AKO_INT_TEST") RegisterFailHandler(Fail) RunSpecs(t, "Atlas Operator Cluster-Wide Integration Test Suite") @@ -79,7 +77,6 @@ func TestAPIs(t *testing.T) { var _ = BeforeSuite(func() { if !control.Enabled("AKO_INT_TEST") { fmt.Println("Skipping int BeforeSuite, AKO_INT_TEST is not set") - return } diff --git a/test/int/integration_suite_test.go b/test/int/integration_suite_test.go index 513c337036..e3d5c8ba02 100644 --- a/test/int/integration_suite_test.go +++ b/test/int/integration_suite_test.go @@ -86,15 +86,12 @@ var ( ) func TestAPIs(t *testing.T) { + control.SkipTestUnless(t, "AKO_INT_TEST") err := akov2.AddToScheme(scheme.Scheme) if err != nil { t.Fatal(err) } - if !control.Enabled("AKO_INT_TEST") { - t.Skip("Skipping int tests, AKO_INT_TEST is not set") - } - RegisterFailHandler(Fail) RunSpecs(t, "Atlas Operator Namespaced Integration Test Suite") }