From fef8633dd7869564241881f2810fbf02ecbbc2db 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 This PR add end to end testing. We use github action CI called end to end, which is executed in every PR. Github CI's ubuntu now support KVM, so tests are quite fast. The process is: setup kind and libvirt Deploy vCsim, which is used has vcenter Deploy registry where we push agent container Build container of agent and push it to registry Build container of agent-service and deploy it on kind run tests The tests are using ginkgo. There are few helper methods implemented to help manage VM and sources. The first end to end test flow: Before each test: Create source Download OVA image & extract it Run libvirt VM using the ISO from OVA Wait for VM IP Wait for planner-agent service to be running Test flow: Send the vCenter credentials to agent Wait until the source is reporting the up-to-date state Signed-off-by: Ondra Machacek --- .dockerignore | 2 +- .github/workflows/go.yml | 10 + .github/workflows/kind.yml | 93 +++++++ Makefile | 26 +- data/ignition.template | 9 + deploy/k8s/migration-planner.yaml.template | 7 +- go.mod | 5 + go.sum | 3 + internal/image/ova.go | 5 + test/e2e/README.md | 20 ++ test/e2e/data/vm.xml | 34 +++ test/e2e/e2e_agent_test.go | 278 +++++++++++++++++++++ test/e2e/e2e_suite_test.go | 24 ++ test/e2e/e2e_test.go | 70 ++++++ test/e2e/e2e_utils_test.go | 100 ++++++++ 15 files changed, 679 insertions(+), 7 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..bcaff0b 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 @@ -36,6 +41,11 @@ jobs: - name: Check out code uses: actions/checkout@v4 + - name: Setup libvirt + run: | + sudo apt update + sudo apt install libvirt-dev + - name: Run golangci-lint uses: golangci/golangci-lint-action@v6.1.1 with: diff --git a/.github/workflows/kind.yml b/.github/workflows/kind.yml new file mode 100644 index 0000000..c5cfd7e --- /dev/null +++ b/.github/workflows/kind.yml @@ -0,0 +1,93 @@ +name: Run e2e test + +on: + workflow_dispatch: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + env: + MIGRATION_PLANNER_API_IMAGE: "custom/migration-planner-api" + MIGRATION_PLANNER_API_IMAGE_PULL_POLICY: "Never" + PODMAN: "docker" + + steps: + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + + - name: Checkout the code + uses: actions/checkout@v2 + + - name: Set env variables + run: | + export "REGISTRY_IP=$(ip addr show eth0 | grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+')" + echo "REGISTRY_IP=${REGISTRY_IP}" >> $GITHUB_ENV + echo "MIGRATION_PLANNER_AGENT_IMAGE=${REGISTRY_IP}:5000/agent" >> $GITHUB_ENV + echo "INSECURE_REGISTRY=${REGISTRY_IP}:5000" >> $GITHUB_ENV + + - name: Ignore insecure registry + run: | + cat << EOF > daemon.json + { + "insecure-registries" : [ "${INSECURE_REGISTRY}" ] + } + EOF + sudo mv daemon.json /etc/docker/daemon.json + sudo systemctl daemon-reload + sudo systemctl restart docker + + - 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: Deploy vcsim + run: | + 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 & + + - name: Deploy registry + 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 & + + - name: Build assisted-migration containers + run: | + make migration-planner-agent-container MIGRATION_PLANNER_AGENT_IMAGE=$MIGRATION_PLANNER_AGENT_IMAGE + make migration-planner-api-container MIGRATION_PLANNER_API_IMAGE=$MIGRATION_PLANNER_API_IMAGE + docker push $MIGRATION_PLANNER_AGENT_IMAGE + kind load docker-image $MIGRATION_PLANNER_API_IMAGE + docker rmi $MIGRATION_PLANNER_API_IMAGE + + - name: Deploy assisted-migration + run: | + make deploy-on-kind MIGRATION_PLANNER_API_IMAGE=$MIGRATION_PLANNER_API_IMAGE MIGRATION_PLANNER_AGENT_IMAGE=$MIGRATION_PLANNER_AGENT_IMAGE MIGRATION_PLANNER_API_IMAGE_PULL_POLICY=$MIGRATION_PLANNER_API_IMAGE_PULL_POLICY INSECURE_REGISTRY=$INSECURE_REGISTRY + kubectl wait --for=condition=Ready pods --all --timeout=240s + 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=${REGISTRY_IP} diff --git a/Makefile b/Makefile index 1cb2cc1..0272bdb 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,13 @@ TIMEOUT ?= 30m VERBOSE ?= false MIGRATION_PLANNER_AGENT_IMAGE ?= quay.io/kubev2v/migration-planner-agent MIGRATION_PLANNER_API_IMAGE ?= quay.io/kubev2v/migration-planner-api +MIGRATION_PLANNER_API_IMAGE_PULL_POLICY ?= Always MIGRATION_PLANNER_UI_IMAGE ?= quay.io/kubev2v/migration-planner-ui +INSECURE_REGISTRY ?= 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 +74,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 +86,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 +99,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|@INSECURE_REGISTRY@|$(INSECURE_REGISTRY)|g; s|@MIGRATION_PLANNER_API_IMAGE_PULL_POLICY@|$(MIGRATION_PLANNER_API_IMAGE_PULL_POLICY)|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_PULL_POLICY@|$(MIGRATION_PLANNER_API_IMAGE_PULL_POLICY)|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/data/ignition.template b/data/ignition.template index 1c6292f..ce9e819 100644 --- a/data/ignition.template +++ b/data/ignition.template @@ -41,6 +41,15 @@ storage: group: name: core files: + {{if .InsecureRegistry}} + - path: /etc/containers/registries.conf.d/myregistry.conf + overwrite: true + contents: + inline: | + [[registry]] + location = "{{.InsecureRegistry}}" + insecure = true + {{end}} - path: /etc/ssh/sshd_config.d/40-rhcos-defaults.conf overwrite: true contents: diff --git a/deploy/k8s/migration-planner.yaml.template b/deploy/k8s/migration-planner.yaml.template index 2b35dc4..12d83fc 100644 --- a/deploy/k8s/migration-planner.yaml.template +++ b/deploy/k8s/migration-planner.yaml.template @@ -22,7 +22,7 @@ spec: cpu: 300m memory: 400Mi image: @MIGRATION_PLANNER_API_IMAGE@ - imagePullPolicy: Always + imagePullPolicy: @MIGRATION_PLANNER_API_IMAGE_PULL_POLICY@ ports: - containerPort: 3443 livenessProbe: @@ -36,6 +36,11 @@ spec: secretKeyRef: name: migration-planner-secret key: config_server + - name: MIGRATION_PLANNER_AGENT_IMAGE + value: @MIGRATION_PLANNER_AGENT_IMAGE@ + - name: INSECURE_REGISTRY + value: @INSECURE_REGISTRY@ + volumeMounts: 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/internal/image/ova.go b/internal/image/ova.go index e55b76b..b9a06be 100644 --- a/internal/image/ova.go +++ b/internal/image/ova.go @@ -33,6 +33,7 @@ type IgnitionData struct { SshKey string PlannerService string MigrationPlannerAgentImage string + InsecureRegistry string } type Image interface { @@ -128,6 +129,10 @@ func (o *Ova) generateIgnition() (string, error) { MigrationPlannerAgentImage: util.GetEnv("MIGRATION_PLANNER_AGENT_IMAGE", "quay.io/kubev2v/migration-planner-agent"), } + if insecureRegistry := os.Getenv("INSECURE_REGISTRY"); insecureRegistry != "" { + ignData.InsecureRegistry = insecureRegistry + } + var buf bytes.Buffer t, err := template.New("ignition.template").ParseFiles("data/ignition.template") if err != nil { 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..1d22a77 --- /dev/null +++ b/test/e2e/e2e_agent_test.go @@ -0,0 +1,278 @@ +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 func() { + _ = 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 func() { + _ = 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..3bd6d85 --- /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..f215b1c --- /dev/null +++ b/test/e2e/e2e_utils_test.go @@ -0,0 +1,100 @@ +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), "