cmd/e2e-test/rune2e/command.go (229 lines of code) (raw):
package rune2e
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/go-logr/logr"
"github.com/integrii/flaggy"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"github.com/aws/eks-hybrid/internal/cli"
"github.com/aws/eks-hybrid/test/e2e"
"github.com/aws/eks-hybrid/test/e2e/cluster"
"github.com/aws/eks-hybrid/test/e2e/run"
)
const (
defaultNodeadmAMDURL = "https://hybrid-assets.eks.amazonaws.com/releases/latest/bin/linux/amd64/nodeadm"
defaultNodeadmARMURL = "https://hybrid-assets.eks.amazonaws.com/releases/latest/bin/linux/arm64/nodeadm"
defaultClusterNamePrefix = "nodeadm-e2e-tests"
defaultRegion = "us-west-2"
defaultK8sVersion = "1.31"
defaultCNI = "cilium"
defaultTimeout = time.Minute * 60
defaultTestProcs = 1
defaultTestsBinaryOrPath = "./test/e2e/suite/nodeadm"
)
type command struct {
artifactsDir string
clusterName string
cni string
ginkgoBinaryPath string
k8sVersion string
logsBucket string
noColor bool
nodeadmAMDURL string
nodeadmARMURL string
region string
setupConfigFile string
skipCleanup bool
skippedTests string
subCmd *flaggy.Subcommand
testConfigFile string
testLabelFilter string
testProcs int
testsBinaryOrPath string
timeout time.Duration
}
func NewCommand() *command {
cmd := &command{
clusterName: fmt.Sprintf("%s-%s", defaultClusterNamePrefix, strings.ReplaceAll(defaultK8sVersion, ".", "-")),
cni: defaultCNI,
k8sVersion: defaultK8sVersion,
nodeadmAMDURL: defaultNodeadmAMDURL,
nodeadmARMURL: defaultNodeadmARMURL,
skipCleanup: false,
subCmd: flaggy.NewSubcommand("run-e2e"),
testProcs: defaultTestProcs,
timeout: defaultTimeout,
testsBinaryOrPath: defaultTestsBinaryOrPath,
}
cmd.subCmd.Description = "Run E2E tests"
cmd.subCmd.String(&cmd.clusterName, "n", "name", "Cluster name (optional)")
cmd.subCmd.String(&cmd.region, "r", "region", "AWS region (optional)")
cmd.subCmd.String(&cmd.k8sVersion, "k", "kubernetes-version", "Kubernetes version (optional)")
cmd.subCmd.String(&cmd.cni, "c", "cni", "CNI plugin (optional)")
cmd.subCmd.String(&cmd.nodeadmAMDURL, "", "nodeadm-amd-url", "NodeADM AMD URL (optional)")
cmd.subCmd.String(&cmd.nodeadmARMURL, "", "nodeadm-arm-url", "NodeADM ARM URL (optional)")
cmd.subCmd.String(&cmd.logsBucket, "b", "logs-bucket", "S3 bucket for logs (optional)")
cmd.subCmd.String(&cmd.artifactsDir, "a", "artifacts-dir", "Directory for artifacts (optional, defaults to a new temp directory)")
cmd.subCmd.String(&cmd.skippedTests, "s", "skipped-tests", "ginkgo regex to skip tests (optional)")
cmd.subCmd.Duration(&cmd.timeout, "", "timeout", "Timeout for the test (optional)")
cmd.subCmd.String(&cmd.testLabelFilter, "f", "test-filter", "Filter for the test (optional)")
cmd.subCmd.Bool(&cmd.skipCleanup, "", "skip-cleanup", "Skip cleanup (optional)")
cmd.subCmd.String(&cmd.testsBinaryOrPath, "", "tests-binary", "Path to the tests binary (optional)")
cmd.subCmd.Bool(&cmd.noColor, "", "no-color", "Disable color output (optional)")
cmd.subCmd.Int(&cmd.testProcs, "p", "procs", "Number of processes to run (optional)")
cmd.subCmd.String(&cmd.setupConfigFile, "", "setup-config", "Path to a YAML file containing cluster.TestResources configuration (optional)")
cmd.subCmd.String(&cmd.testConfigFile, "", "test-config", "Path to a YAML file containing suite.TestConfig configuration (optional)")
cmd.subCmd.String(&cmd.ginkgoBinaryPath, "", "ginkgo-binary", "Path to the ginkgo binary (defaults to the ginkgo binary in the same folder as e2e-test or in the PATH)")
return cmd
}
func (c *command) Flaggy() *flaggy.Subcommand {
return c.subCmd
}
func (c *command) Commands() []cli.Command {
return []cli.Command{c}
}
func (c *command) Run(log *zap.Logger, opts *cli.GlobalOptions) error {
ctx := context.Background()
logger := e2e.NewLogger(e2e.LoggerConfig{NoColor: c.noColor})
awsCfg, err := e2e.NewAWSConfig(ctx,
config.WithRegion(c.region),
// We use a custom AppId so the requests show that they were
// made by this command in the user-agent
config.WithAppID("nodeadm-e2e-test-run-cmd"),
)
if err != nil {
return fmt.Errorf("reading AWS configuration: %w", err)
}
if c.region == "" {
c.region = awsCfg.Region
}
testResources, err := c.loadSetupConfig(logger)
if err != nil {
return fmt.Errorf("loading test resources configuration: %w", err)
}
testConfig, err := c.loadTestConfig(testResources, logger)
if err != nil {
return fmt.Errorf("loading test configuration: %w", err)
}
artifactsDir, err := c.getArtifactsDir(c.artifactsDir, logger)
if err != nil {
return fmt.Errorf("getting artifacts directory: %w", err)
}
testConfig.ArtifactsFolder = artifactsDir
ginkgoBinaryPath, err := c.getGinkgoBinaryPath()
if err != nil {
return fmt.Errorf("getting ginkgo binary path: %w", err)
}
// Run E2E tests
e2e := run.E2E{
AwsCfg: awsCfg,
Logger: logger,
NoColor: c.noColor,
Paths: run.E2EPaths{
Ginkgo: ginkgoBinaryPath,
TestsBinaryOrSource: c.testsBinaryOrPath,
},
TestConfig: testConfig,
TestLabelFilter: c.testLabelFilter,
TestProcs: c.testProcs,
Timeout: c.timeout,
TestResources: testResources,
SkipCleanup: c.skipCleanup,
SkippedTests: c.skippedTests,
}
e2eResult, testErr := e2e.Run(ctx)
// Always try to output the results
outputErr := e2e.PrintResults(ctx, e2eResult)
if outputErr != nil {
logger.Error(outputErr, "outputting E2E results")
}
if testErr != nil {
return fmt.Errorf("running E2E tests: %w", testErr)
}
return nil
}
func (c *command) getArtifactsDir(artifactsDir string, logger logr.Logger) (string, error) {
var err error
if artifactsDir == "" {
artifactsDir, err = os.MkdirTemp("", "eks-hybrid-e2e-*")
if err != nil {
return "", fmt.Errorf("creating temp directory: %w", err)
}
logger.Info("Created temporary test directory", "path", artifactsDir)
}
artifactsDir, err = filepath.Abs(artifactsDir)
if err != nil {
return "", fmt.Errorf("getting absolute path for artifacts: %w", err)
}
return artifactsDir, nil
}
// loadSetupConfig loads the TestResources configuration from a file.
// It validates that no individual resource flags are set when using a config file.
func (c *command) loadSetupConfig(logger logr.Logger) (cluster.TestResources, error) {
// Initialize default test resources
testResources := cluster.TestResources{
ClusterName: c.clusterName,
ClusterRegion: c.region,
KubernetesVersion: c.k8sVersion,
Cni: c.cni,
}
testResources = cluster.SetTestResourcesDefaults(testResources)
if c.setupConfigFile != "" {
// Validate that individual resource flags are not also set
defaultClusterName := fmt.Sprintf("%s-%s", defaultClusterNamePrefix, strings.ReplaceAll(defaultK8sVersion, ".", "-"))
if c.clusterName != defaultClusterName ||
c.region != defaultRegion ||
c.k8sVersion != defaultK8sVersion ||
c.cni != defaultCNI {
return testResources, fmt.Errorf("cannot specify both setup-config file and individual cluster resource flags (name, region, kubernetes-version, cni, eks-endpoint)")
}
// Load test resources from file
var err error
testResources, err = cluster.LoadTestResources(c.setupConfigFile)
if err != nil {
return testResources, fmt.Errorf("reading setup config file: %w", err)
}
logger.Info("Loaded test resources configuration from file", "path", c.setupConfigFile)
}
return testResources, nil
}
// loadTestConfig loads the TestConfig configuration from a file.
// It validates that no individual test config flags are set when using a config file.
func (c *command) loadTestConfig(testResources cluster.TestResources, logger logr.Logger) (e2e.TestConfig, error) {
testConfig := e2e.TestConfig{
ClusterName: testResources.ClusterName,
ClusterRegion: testResources.ClusterRegion,
Endpoint: testResources.EKS.Endpoint,
NodeadmUrlAMD: c.nodeadmAMDURL,
NodeadmUrlARM: c.nodeadmARMURL,
LogsBucket: c.logsBucket,
}
if c.testConfigFile != "" {
// Validate that individual test config flags are not also set
if c.nodeadmAMDURL != defaultNodeadmAMDURL ||
c.nodeadmARMURL != defaultNodeadmARMURL ||
c.logsBucket != "" ||
c.artifactsDir != "" {
return testConfig, fmt.Errorf("cannot specify both test-config file and individual test config flags (nodeadm-amd-url, nodeadm-arm-url, logs-bucket, artifacts-dir)")
}
// Load test config from file
testConfigData, err := os.ReadFile(c.testConfigFile)
if err != nil {
return testConfig, fmt.Errorf("reading test config file: %w", err)
}
if err := yaml.Unmarshal(testConfigData, &testConfig); err != nil {
return testConfig, fmt.Errorf("unmarshaling test config: %w", err)
}
logger.Info("Loaded test configuration from file", "path", c.testConfigFile)
}
return testConfig, nil
}
func (c *command) getGinkgoBinaryPath() (string, error) {
if c.ginkgoBinaryPath != "" {
return c.ginkgoBinaryPath, nil
}
ex, err := os.Executable()
if err != nil {
return "", fmt.Errorf("getting executable path: %w", err)
}
binPath := filepath.Dir(ex)
ginkgoBinaryPath := filepath.Join(binPath, "ginkgo")
_, err = os.Stat(ginkgoBinaryPath)
if err == nil {
return ginkgoBinaryPath, nil
}
// fallback to ginkgo in PATH
return "ginkgo", nil
}