diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..19f96e15 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,55 @@ +# Copyright 2024 NVIDIA CORPORATION +# +# 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. + +name: End-to-end Tests + +on: + workflow_run: + workflows: [Go] + types: + - completed + branches: + - main + - release-* + +jobs: + e2e-tests: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} && ${{ github.event.workflow_run.event == 'push' }} + steps: + - uses: actions/checkout@v4 + name: Check out code + - name: Calculate build vars + id: vars + run: | + echo "COMMIT_SHORT_SHA=${GITHUB_SHA:0:8}" >> $GITHUB_ENV + - name: Set up Holodeck + uses: NVIDIA/holodeck@main + with: + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + - name: Run e2e tests + env: + LOG_ARTIFACTS: ${{ github.workspace }}/e2e_logs + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + run: | + make -f tests/Makefile e2e-test + - name: Archive test logs + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: e2e-test-logs + path: ./e2e_logs/ + retention-days: 15 diff --git a/.gitignore b/.gitignore index 0b240a2e..cf9fb620 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ kubeconfig bin -.DS_Store \ No newline at end of file +.DS_Store +e2e_logs \ No newline at end of file diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 609f4ba1..87fdb13a 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -93,6 +93,8 @@ type FunLogger struct { Fail chan struct{} // Wg is a WaitGroup that can be used to wait for the loading animation to finish. Wg *sync.WaitGroup + // CI is a boolean that is set to true if the logger is running in a CI environment. + CI bool } // Info prints an information message with no emoji. @@ -130,7 +132,7 @@ func (l *FunLogger) Loading(format string, a ...any) { defer l.Wg.Done() message := fmt.Sprintf(format, a...) // if running in a non-interactive terminal, don't print the loading animation - if !isInteractiveTerminal() && isCILogs() { + if !isInteractiveTerminal() && l.isCILogs() { // print the message with loading emoji printMessage(yellowText, loadingEmoji, message) for { @@ -184,8 +186,11 @@ func isTerminal(w fdWriter) bool { return isatty.IsTerminal(w.Fd()) } -func isCILogs() bool { - return os.Getenv("CI") == "true" +func (l *FunLogger) isCILogs() bool { + if os.Getenv("CI") == "true" { + return true + } + return l.CI } func (l *FunLogger) Exit(code int) { diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 00000000..f308c7d0 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,32 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. +# +# 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. + +GO_CMD ?= go + +E2E_ENV_FILE ?= $(CURDIR)/tests/test_env.yml +LOG_ARTIFACTS ?= $(CURDIR)/e2e_logs + +.PHONY: e2e-test +e2e-test: + @if [ -z ${AWS_SECRET_ACCESS_KEY} ] || [ -z ${AWS_ACCESS_KEY_ID} ]; then \ + echo "[ERR] AWS_SECRET_ACCESS_KEY and AWS_ACCESS_KEY_ID must be defined"; \ + exit 1; \ + fi + @echo "Running e2e tests" + $(GO_CMD) test -v $(CURDIR)/tests -args \ + -env-file=$(E2E_ENV_FILE)\ + -log-artifacts=$(LOG_ARTIFACTS) \ + -ginkgo.focus="Holodeck" \ + -test.timeout=1h \ + -ginkgo.v diff --git a/tests/e2e_test.go b/tests/e2e_test.go new file mode 100644 index 00000000..85edcfc9 --- /dev/null +++ b/tests/e2e_test.go @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. + * + * 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 e2e + +import ( + "flag" + "log" + "os" + "testing" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + + "github.com/NVIDIA/k8s-test-infra/pkg/framework" +) + +var ( + LogArtifactDir = flag.String("log-artifacts", "", "Directory to store logs") + EnvFile = flag.String("env-file", "", "Environment file to use") +) + +func TestMain(m *testing.M) { + // Register test flags, then parse flags. + framework.RegisterClusterFlags(flag.CommandLine) + flag.Parse() + + // check if flags are set and if not cancel the test run + if *EnvFile == "" { + log.Fatal("Required flags not set. Please set -env-file") + } + + os.Exit(m.Run()) +} + +func TestE2E(t *testing.T) { + gomega.RegisterFailHandler(ginkgo.Fail) + // Run tests through the Ginkgo runner with output to console + JUnit for Jenkins + suiteConfig, reporterConfig := ginkgo.GinkgoConfiguration() + // Randomize specs as well as suites + suiteConfig.RandomizeAllSpecs = true + + ginkgo.RunSpecs(t, "nvidia holodeck e2e suite", suiteConfig, reporterConfig) +} diff --git a/tests/holodeck_test.go b/tests/holodeck_test.go new file mode 100644 index 00000000..7087f5e2 --- /dev/null +++ b/tests/holodeck_test.go @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. + * + * 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 e2e + +import ( + "context" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/NVIDIA/holodeck/api/holodeck/v1alpha1" + "github.com/NVIDIA/holodeck/internal/logger" + "github.com/NVIDIA/holodeck/pkg/jyaml" + "github.com/NVIDIA/holodeck/pkg/provider/aws" + "github.com/NVIDIA/holodeck/pkg/provisioner" +) + +// Actual test suite +var _ = Describe("Holodeck", func() { + type options struct { + cachePath string + cachefile string + + cfg v1alpha1.Environment + } + opts := options{} + + // Create a logger + log := logger.NewLogger() + log.CI = true + + Context("When testing an environment", Ordered, func() { + BeforeAll(func(ctx context.Context) { + // + // Read the config file + var err error + opts.cfg, err = jyaml.UnmarshalFromFile[v1alpha1.Environment](*EnvFile) + Expect(err).ToNot(HaveOccurred()) + + // set cache path + if opts.cachePath == "" { + opts.cachePath = filepath.Join(*LogArtifactDir, "holodeck") + } + opts.cachefile = filepath.Join(opts.cachePath, opts.cfg.Name+".yaml") + + opts.cfg.Spec.Provider = v1alpha1.ProviderAWS + }) + + AfterAll(func(ctx context.Context) { + // remove the cache file if the test is successful + if !CurrentGinkgoTestDescription().Failed { + err := os.Remove(opts.cachefile) + Expect(err).ToNot(HaveOccurred()) + } + }) + + Context("and calling dryrun to validate the file", func() { + It("validate the provider", func() { + client, err := aws.New(log, opts.cfg, *EnvFile) + Expect(err).ToNot(HaveOccurred()) + + err = client.DryRun() + Expect(err).ToNot(HaveOccurred()) + }) + + It("validate the provisioner", func() { + err := provisioner.Dryrun(log, opts.cfg) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("and calling provision to create the environment", func() { + AfterAll(func(ctx context.Context) { + // Delete the environment + client, err := aws.New(log, opts.cfg, opts.cachefile) + Expect(err).ToNot(HaveOccurred()) + + err = client.Delete() + Expect(err).ToNot(HaveOccurred()) + }) + It("create the environment", func() { + client, err := aws.New(log, opts.cfg, opts.cachefile) + Expect(err).ToNot(HaveOccurred()) + + err = client.Create() + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) +}) diff --git a/tests/test_env.yml b/tests/test_env.yml new file mode 100644 index 00000000..cde95342 --- /dev/null +++ b/tests/test_env.yml @@ -0,0 +1,21 @@ +apiVersion: holodeck.nvidia.com/v1alpha1 +kind: Environment +metadata: + name: holodeck + description: "Devel infra environment" +spec: + provider: aws + auth: + keyName: eduardoa + privateKey: "/Users/eduardoa/.ssh/eduardoa.pem" + instance: + type: g4dn.xlarge + region: eu-north-1 + ingressIpRanges: + - 213.179.129.0/26 + image: + architecture: amd64 + imageId: ami-0fe8bec493a81c7da + kubernetes: + install: true + installer: microk8s