From b4d89f1e41575d321c96c60466ba96af54f1a02f Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Tue, 22 Oct 2024 14:30:16 +0200 Subject: [PATCH] Add end to end tests Signed-off-by: Ondra Machacek --- .dockerignore | 2 +- .github/workflows/go.yml | 5 + .github/workflows/kind.yml | 54 ++++ Makefile | 24 +- deploy/k8s/migration-planner.yaml.template | 2 + go.mod | 5 + go.sum | 3 + test/e2e/README.md | 20 ++ test/e2e/data/vm.xml | 34 +++ test/e2e/e2e_agent_test.go | 274 +++++++++++++++++++++ test/e2e/e2e_suite_test.go | 24 ++ test/e2e/e2e_test.go | 70 ++++++ test/e2e/e2e_utils_test.go | 98 ++++++++ 13 files changed, 609 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/kind.yml create mode 100644 test/e2e/README.md create mode 100644 test/e2e/data/vm.xml create mode 100644 test/e2e/e2e_agent_test.go create mode 100644 test/e2e/e2e_suite_test.go create mode 100644 test/e2e/e2e_test.go create mode 100644 test/e2e/e2e_utils_test.go diff --git a/.dockerignore b/.dockerignore index 5c665e5..62562dc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1 @@ -rhcos-live.x86_64.iso +**/*iso diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 53109b4..53e0830 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -15,6 +15,11 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 + - name: Setup libvirt + run: | + sudo apt update + sudo apt install libvirt-dev + - name: Prepare run: | make generate diff --git a/.github/workflows/kind.yml b/.github/workflows/kind.yml new file mode 100644 index 0000000..8dcc437 --- /dev/null +++ b/.github/workflows/kind.yml @@ -0,0 +1,54 @@ +name: Run e2e test + +on: + workflow_dispatch: + push: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout the code + uses: actions/checkout@v2 + + - name: Install kubectl + uses: azure/setup-kubectl@v4 + + - name: Create k8s Kind Cluster + uses: helm/kind-action@v1 + with: + cluster_name: kind + + - name: Setup libvirt + run: | + sudo apt update + sudo apt install sshpass libvirt-dev libvirt-daemon libvirt-daemon-system + sudo systemctl restart libvirtd + + - name: Build agent container + run: | + kubectl create deployment registry --image=docker.io/registry + kubectl wait --for=condition=Ready pods --all --timeout=240s + kubectl port-forward --address 0.0.0.0 deploy/registry 5000:5000 & + + export REGISTRY_IP=$(ip addr show eth0 | grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+') + export MIGRATION_PLANNER_AGENT_IMAGE="${REGISTRY_IP}:5000/agent" + make migration-planner-agent-container + podman push $MIGRATION_PLANNER_AGENT_IMAGE + + - name: Deploy + run: | + export REGISTRY_IP=$(ip addr show eth0 | grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+') + export MIGRATION_PLANNER_AGENT_IMAGE="${REGISTRY_IP}:5000/agent" + make deploy-on-kind + kubectl create deployment vcsim --image=docker.io/vmware/vcsim + kubectl wait --for=condition=Ready pods --all --timeout=240s + kubectl port-forward --address 0.0.0.0 deploy/vcsim 8989:8989 & + kubectl port-forward --address 0.0.0.0 service/migration-planner-agent 7443:7443 & + kubectl port-forward --address 0.0.0.0 service/migration-planner 3443:3443 & + + - name: Run test + run: | + sudo make integration-test PLANNER_IP=$(ip addr show eth0 | grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+') diff --git a/Makefile b/Makefile index 1cb2cc1..83eec63 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,9 @@ MIGRATION_PLANNER_AGENT_IMAGE ?= quay.io/kubev2v/migration-planner-agent MIGRATION_PLANNER_API_IMAGE ?= quay.io/kubev2v/migration-planner-api MIGRATION_PLANNER_UI_IMAGE ?= quay.io/kubev2v/migration-planner-ui DOWNLOAD_RHCOS ?= true +KUBECTL ?= kubectl +IFACE ?= eth0 +PODMAN ?= podman SOURCE_GIT_TAG ?=$(shell git describe --always --long --tags --abbrev=7 --match 'v[0-9]*' || echo 'v0.0.0-unknown-$(SOURCE_GIT_COMMIT)') SOURCE_GIT_TREE_STATE ?=$(shell ( ( [ ! -d ".git/" ] || git diff --quiet ) && echo 'clean' ) || echo 'dirty') @@ -69,6 +72,9 @@ ifeq ($(DOWNLOAD_RHCOS), true) curl --silent -C - -O https://mirror.openshift.com/pub/openshift-v4/dependencies/rhcos/latest/rhcos-live.x86_64.iso endif +integration-test: ginkgo + $(GINKGO) -focus=$(FOCUS) run test/e2e + build: bin image go build -buildvcs=false $(GO_BUILD_FLAGS) -o $(GOBIN) ./cmd/... @@ -78,10 +84,10 @@ build-api: bin # rebuild container only on source changes bin/.migration-planner-agent-container: bin Containerfile.agent go.mod go.sum $(GO_FILES) - podman build -f Containerfile.agent -t $(MIGRATION_PLANNER_AGENT_IMAGE):latest + $(PODMAN) build . -f Containerfile.agent -t $(MIGRATION_PLANNER_AGENT_IMAGE):latest bin/.migration-planner-api-container: bin Containerfile.api go.mod go.sum $(GO_FILES) - podman build -f Containerfile.api -t $(MIGRATION_PLANNER_API_IMAGE):latest + $(PODMAN) build . -f Containerfile.api -t $(MIGRATION_PLANNER_API_IMAGE):latest migration-planner-api-container: bin/.migration-planner-api-container migration-planner-agent-container: bin/.migration-planner-agent-container @@ -91,11 +97,19 @@ build-containers: migration-planner-api-container migration-planner-agent-contai .PHONY: build-containers push-containers: build-containers - podman push $(MIGRATION_PLANNER_API_IMAGE):latest - podman push $(MIGRATION_PLANNER_AGENT_IMAGE):latest + $(PODMAN) push $(MIGRATION_PLANNER_API_IMAGE):latest + $(PODMAN) push $(MIGRATION_PLANNER_AGENT_IMAGE):latest + +deploy-on-kind: + sed 's|@MIGRATION_PLANNER_AGENT_IMAGE@|$(MIGRATION_PLANNER_AGENT_IMAGE)|g; s|@MIGRATION_PLANNER_API_IMAGE@|$(MIGRATION_PLANNER_API_IMAGE)|g' deploy/k8s/migration-planner.yaml.template > deploy/k8s/migration-planner.yaml + $(KUBECTL) apply -f 'deploy/k8s/*-service.yaml' + $(KUBECTL) apply -f 'deploy/k8s/*-secret.yaml' + @config_server=$$(ip addr show ${IFACE}| grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+'); \ + $(KUBECTL) create secret generic migration-planner-secret --from-literal=config_server=http://$$config_server:7443 || true + $(KUBECTL) apply -f deploy/k8s/ deploy-on-openshift: - sed 's|@MIGRATION_PLANNER_API_IMAGE@|$(MIGRATION_PLANNER_API_IMAGE)|g' deploy/k8s/migration-planner.yaml.template > deploy/k8s/migration-planner.yaml + sed 's|@MIGRATION_PLANNER_AGENT_IMAGE@|$(MIGRATION_PLANNER_AGENT_IMAGE)|g; s|@MIGRATION_PLANNER_API_IMAGE@|$(MIGRATION_PLANNER_API_IMAGE)|g' deploy/k8s/migration-planner.yaml.template > deploy/k8s/migration-planner.yaml sed 's|@MIGRATION_PLANNER_UI_IMAGE@|$(MIGRATION_PLANNER_UI_IMAGE)|g' deploy/k8s/migration-planner-ui.yaml.template > deploy/k8s/migration-planner-ui.yaml oc apply -f 'deploy/k8s/*-service.yaml' oc apply -f 'deploy/k8s/*-secret.yaml' diff --git a/deploy/k8s/migration-planner.yaml.template b/deploy/k8s/migration-planner.yaml.template index 2b35dc4..fe2878e 100644 --- a/deploy/k8s/migration-planner.yaml.template +++ b/deploy/k8s/migration-planner.yaml.template @@ -36,6 +36,8 @@ spec: secretKeyRef: name: migration-planner-secret key: config_server + - name: MIGRATION_PLANNER_AGENT_IMAGE + value: @MIGRATION_PLANNER_AGENT_IMAGE@ volumeMounts: - name: migration-planner-config mountPath: "/.migration-planner/config.yaml" diff --git a/go.mod b/go.mod index 4f6333b..225749b 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,11 @@ require ( github.com/go-chi/render v1.0.3 github.com/google/uuid v1.6.0 github.com/konveyor/forklift-controller v0.0.0-20221102112227-e73b65a01cda + github.com/libvirt/libvirt-go v7.4.0+incompatible github.com/lthibault/jitterbug v2.0.0+incompatible github.com/oapi-codegen/nethttp-middleware v1.0.2 github.com/oapi-codegen/runtime v1.1.1 + github.com/onsi/ginkgo v1.16.5 github.com/onsi/ginkgo/v2 v2.15.0 github.com/onsi/gomega v1.32.0 github.com/openshift/assisted-image-service v0.0.0-20240827125623-ad5c4b36a817 @@ -50,6 +52,7 @@ require ( github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/cors v1.3.1 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -94,6 +97,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nxadm/tail v1.4.11 // indirect github.com/openshift/api v0.0.0-20230613151523-ba04973d3ed1 // indirect github.com/openshift/custom-resource-status v1.1.2 // indirect github.com/pborman/uuid v1.2.1 // indirect @@ -125,6 +129,7 @@ require ( google.golang.org/protobuf v1.33.0 // indirect gopkg.in/djherbis/times.v1 v1.3.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.28.3 // indirect diff --git a/go.sum b/go.sum index 5f22413..1932c66 100644 --- a/go.sum +++ b/go.sum @@ -236,6 +236,8 @@ github.com/kubev2v/forklift v0.0.0-20240729073638-8978e272380e/go.mod h1:1jmlC7L github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/libvirt/libvirt-go v7.4.0+incompatible h1:crnSLkwPqCdXtg6jib/FxBG/hweAc/3Wxth1AehCXL4= +github.com/libvirt/libvirt-go v7.4.0+incompatible/go.mod h1:34zsnB4iGeOv7Byj6qotuW8Ya4v4Tr43ttjz/F0wjLE= github.com/lthibault/jitterbug v2.0.0+incompatible h1:qouq51IKzlMx25+15jbxhC/d79YyTj0q6XFoptNqaUw= github.com/lthibault/jitterbug v2.0.0+incompatible/go.mod h1:2l7akWd27PScEs6YkjyUVj/8hKgNhbbQ3KiJgJtlf6o= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -461,6 +463,7 @@ golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= diff --git a/test/e2e/README.md b/test/e2e/README.md new file mode 100644 index 0000000..6b63b12 --- /dev/null +++ b/test/e2e/README.md @@ -0,0 +1,20 @@ +# Running integration tests +The integration tests are executed against deployed `planner-api`. The planner api can be deployed +as container or running as binary. + +## Requiremets + +``` +dnf install -y libvirt-devel +sudo usermod -a -G libvirt $USER +``` + +Running planner api, either as container or binary: +``` +bin/planner-api +``` + +## Executing tests +``` +PLANNER_IP=1.2.3.4 make integration-tests +``` diff --git a/test/e2e/data/vm.xml b/test/e2e/data/vm.xml new file mode 100644 index 0000000..39f8ed7 --- /dev/null +++ b/test/e2e/data/vm.xml @@ -0,0 +1,34 @@ + + coreos-vm + 4096 + 2 + + + + + + + hvm + + + + + + + + + /usr/bin/qemu-system-x86_64 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/e2e/e2e_agent_test.go b/test/e2e/e2e_agent_test.go new file mode 100644 index 0000000..49e7a09 --- /dev/null +++ b/test/e2e/e2e_agent_test.go @@ -0,0 +1,274 @@ +package e2e_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + + "github.com/google/uuid" + api "github.com/kubev2v/migration-planner/api/v1alpha1" + internalclient "github.com/kubev2v/migration-planner/internal/api/client" + "github.com/kubev2v/migration-planner/internal/client" + libvirt "github.com/libvirt/libvirt-go" + . "github.com/onsi/ginkgo/v2" +) + +const ( + vmName string = "coreos-vm" +) + +var ( + home string = os.Getenv("HOME") + defaultConfigPath string = filepath.Join(home, ".config/planner/client.yaml") + defaultIsoPath string = "/tmp/agent.iso" + defaultOvaPath string = filepath.Join(home, "myimage.ova") + defaultServiceUrl string = fmt.Sprintf("http://%s:3443", os.Getenv("PLANNER_IP")) +) + +type PlannerAgent interface { + Run(string) error + Login(url string, user string, pass string) error + Remove() error + GetIp() (string, error) + IsServiceRunning(string, string) bool + DumpLogs(string) +} + +type PlannerService interface { + Create(name string) (string, error) + RemoveSources() error + GetSource() (*api.Source, error) +} + +type plannerService struct { + c *internalclient.ClientWithResponses +} + +type plannerAgentLibvirt struct { + c *internalclient.ClientWithResponses + name string + con *libvirt.Connect +} + +func NewPlannerAgent(configPath string, name string) (*plannerAgentLibvirt, error) { + _ = createConfigFile(configPath) + + c, err := client.NewFromConfigFile(configPath) + if err != nil { + return nil, fmt.Errorf("creating client: %w", err) + } + + conn, err := libvirt.NewConnect("qemu:///system") + if err != nil { + return nil, fmt.Errorf("failed to connect to hypervisor: %v", err) + } + + return &plannerAgentLibvirt{c: c, name: name, con: conn}, nil +} + +func (p *plannerAgentLibvirt) Run(sourceId string) error { + if err := p.prepareImage(sourceId); err != nil { + return err + } + + err := CreateVm(p.con) + if err != nil { + return err + } + + return nil +} + +func (p *plannerAgentLibvirt) prepareImage(sourceId string) error { + // Create OVA: + file, err := os.Create(defaultOvaPath) + if err != nil { + return err + } + defer os.Remove(file.Name()) + + // Download OVA + res, err := p.c.GetSourceImage(context.TODO(), uuid.MustParse(sourceId)) + if err != nil { + return fmt.Errorf("error getting source image: %w", err) + } + defer res.Body.Close() + + if _, err = io.Copy(file, res.Body); err != nil { + return fmt.Errorf("error writing to file: %w", err) + } + + // Untar ISO from OVA + if err = Untar(file, defaultIsoPath, "AgentVM-1.iso"); err != nil { + return fmt.Errorf("error uncompressing the file: %w", err) + } + + return nil +} + +func (p *plannerAgentLibvirt) Login(url string, user string, pass string) error { + agentIP, err := p.GetIp() + if err != nil { + return fmt.Errorf("failed to get agent IP: %w", err) + } + + credentials := map[string]string{ + "url": url, + "username": user, + "password": pass, + } + + jsonData, err := json.Marshal(credentials) + if err != nil { + return fmt.Errorf("failed to marshal credentials: %w", err) + } + + resp, err := http.NewRequest( + "PUT", + fmt.Sprintf("http://%s:3333/api/v1/credentials", agentIP), + bytes.NewBuffer(jsonData), + ) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + resp.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + response, err := client.Do(resp) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer response.Body.Close() + + return nil +} + +func (p *plannerAgentLibvirt) RestartService() error { + return nil +} + +func (p *plannerAgentLibvirt) Remove() error { + defer p.con.Close() + domain, err := p.con.LookupDomainByName(p.name) + if err != nil { + return err + } + defer domain.Free() + + if state, _, err := domain.GetState(); err == nil && state == libvirt.DOMAIN_RUNNING { + if err := domain.Destroy(); err != nil { + return err + } + } + + if err := domain.Undefine(); err != nil { + return err + } + + // Remove the ISO file if it exists + if _, err := os.Stat(defaultIsoPath); err == nil { + if err := os.Remove(defaultIsoPath); err != nil { + return fmt.Errorf("failed to remove ISO file: %w", err) + } + } + + return nil +} + +func (p *plannerAgentLibvirt) GetIp() (string, error) { + domain, err := p.con.LookupDomainByName(p.name) + if err != nil { + return "", err + } + defer domain.Free() + + // Get VM IP: + ifaceAddresses, err := domain.ListAllInterfaceAddresses(libvirt.DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE) + if err != nil { + return "", err + } + + for _, iface := range ifaceAddresses { + for _, addr := range iface.Addrs { + if addr.Type == libvirt.IP_ADDR_TYPE_IPV4 { + return addr.Addr, nil + } + } + } + return "", fmt.Errorf("No IP found") +} + +func (p *plannerAgentLibvirt) IsServiceRunning(agentIp string, service string) bool { + _, err := RunCommand(agentIp, fmt.Sprintf("systemctl --user is-active --quiet %s", service)) + return err == nil +} + +func (p *plannerAgentLibvirt) DumpLogs(agentIp string) { + stdout, _ := RunCommand(agentIp, "journalctl --no-pager --user -u planner-agent") + fmt.Fprintf(GinkgoWriter, "Journal: %v\n", stdout) +} + +func NewPlannerService(configPath string) (*plannerService, error) { + _ = createConfigFile(configPath) + c, err := client.NewFromConfigFile(configPath) + if err != nil { + return nil, fmt.Errorf("creating client: %w", err) + } + + return &plannerService{c: c}, nil +} + +func (s *plannerService) Create(name string) (string, error) { + ctx := context.TODO() + body := api.SourceCreate{Name: name} + _, err := s.c.CreateSource(ctx, body) + if err != nil { + return "", fmt.Errorf("Error creating source") + } + + source, err := s.GetSource() + if err != nil { + return "", err + } + + return source.Id.String(), nil +} + +func (s *plannerService) GetSource() (*api.Source, error) { + ctx := context.TODO() + res, err := s.c.ListSourcesWithResponse(ctx) + if err != nil || res.HTTPResponse.StatusCode != 200 { + return nil, fmt.Errorf("Error listing sources") + } + + if len(*res.JSON200) == 0 { + return nil, fmt.Errorf("No sources found") + } + + return &(*res.JSON200)[0], nil +} + +func (s *plannerService) RemoveSources() error { + _, err := s.c.DeleteSourcesWithResponse(context.TODO()) + return err +} + +func createConfigFile(configPath string) error { + // Ensure the directory exists + dir := filepath.Dir(configPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("creating directory structure: %w", err) + } + + // Create configuration + if _, err := os.Stat(configPath); os.IsNotExist(err) { + return os.WriteFile(configPath, []byte(fmt.Sprintf("service:\n server: %s", defaultServiceUrl)), 0644) + } + + return nil +} diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go new file mode 100644 index 0000000..ae78769 --- /dev/null +++ b/test/e2e/e2e_suite_test.go @@ -0,0 +1,24 @@ +package e2e_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestE2e(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "E2e Suite") +} + +// AfterFailed is a function that it's called on JustAfterEach to run a +// function if the test fail. For example, retrieving logs. +func AfterFailed(body func()) { + JustAfterEach(func() { + if CurrentSpecReport().Failed() { + By("Running AfterFailed function") + body() + } + }) +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go new file mode 100644 index 0000000..00c6e52 --- /dev/null +++ b/test/e2e/e2e_test.go @@ -0,0 +1,70 @@ +package e2e_test + +import ( + "fmt" + "os" + + "github.com/kubev2v/migration-planner/api/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("e2e", func() { + + var ( + svc PlannerService + agent PlannerAgent + sourceId string + agentIP string + err error + systemIP = os.Getenv("PLANNER_IP") + ) + + BeforeEach(func() { + svc, err = NewPlannerService(defaultConfigPath) + Expect(err).To(BeNil()) + agent, err = NewPlannerAgent(defaultConfigPath, vmName) + Expect(err).To(BeNil()) + sourceId, err = svc.Create("testsource") + Expect(err).To(BeNil()) + Expect(sourceId).ToNot(BeNil()) + err = agent.Run(sourceId) + Expect(err).To(BeNil()) + Eventually(func() string { + agentIP, err = agent.GetIp() + if err != nil { + return "" + } + return agentIP + }, "1m", "3s").ShouldNot(BeEmpty()) + Expect(agentIP).ToNot(BeEmpty()) + Eventually(func() bool { + return agent.IsServiceRunning(agentIP, "planner-agent") + }, "3m", "2s").Should(BeTrue()) + }) + + AfterEach(func() { + _ = svc.RemoveSources() + //_ = agent.Remove() + }) + + AfterFailed(func() { + agent.DumpLogs(agentIP) + }) + + Context("Flow", func() { + It("Up to date", func() { + r := agent.IsServiceRunning(agentIP, "planner-agent") + Expect(r).To(BeTrue()) + err = agent.Login(fmt.Sprintf("https://%s:8989/sdk", systemIP), "user", "pass") + Expect(err).To(BeNil()) + Eventually(func() bool { + source, err := svc.GetSource() + if err != nil { + return false + } + return source.Status == v1alpha1.SourceStatusUpToDate + }, "1m", "2s").Should(BeTrue()) + }) + }) +}) diff --git a/test/e2e/e2e_utils_test.go b/test/e2e/e2e_utils_test.go new file mode 100644 index 0000000..1cdab96 --- /dev/null +++ b/test/e2e/e2e_utils_test.go @@ -0,0 +1,98 @@ +package e2e_test + +import ( + "archive/tar" + "bytes" + "fmt" + "io" + "os" + "os/exec" + "strings" + + libvirt "github.com/libvirt/libvirt-go" +) + +func Untar(file *os.File, destFile string, fileName string) error { + file.Seek(0, 0) + tarReader := tar.NewReader(file) + containsOvf := false + + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("error reading tar header: %w", err) + } + + switch header.Typeflag { + case tar.TypeReg: + if strings.HasSuffix(header.Name, ".ovf") { + // Validate OVF file + ovfContent, err := io.ReadAll(tarReader) + if err != nil { + return fmt.Errorf("error reading OVF file: %w", err) + } + + // Basic validation: check if the content contains essential OVF elements + if !strings.Contains(string(ovfContent), "