diff --git a/agent/06_agent_create_cluster.sh b/agent/06_agent_create_cluster.sh index 7f6caf1ce..042281fca 100755 --- a/agent/06_agent_create_cluster.sh +++ b/agent/06_agent_create_cluster.sh @@ -633,41 +633,21 @@ case "${AGENT_E2E_TEST_BOOT_MODE}" in check_assisted_install_UI - # Temporarily create a dummy kubeconfig and kubeadmin-password file for the CI - auth_dir=$SCRIPTDIR/$OCP_DIR/auth - mkdir -p $auth_dir - cfg=$auth_dir/kubeconfig - cat << EOF >> ${cfg} -clusters: -- cluster: - certificate-authority-data: LS0tLS1CRUdJTiBGSUNBVLS0tLQo= - server: https://api.test.redhat.com:6443 - name: test -contexts: -- context: - cluster: test - user: admin - name: admin -current-context: admin -preferences: {} -users: -- name: admin - user: - client-certificate-data: LS0tLS1CRUdJTiBNBVEUtLS0tLQo= - client-key-data: LS0tLS1CRUdJTiURSBVktLS0tLQo= -EOF - echo "dummy-kubeadmin-password" > $auth_dir/kubeadmin-password + go get github.com/go-rod/rod \ + github.com/go-resty/resty/v2 \ + github.com/go-rod/rod/lib/launcher \ + github.com/go-rod/rod/lib/proto \ + github.com/go-rod/rod/lib/utils + + mkdir -p $OCP_DIR/auth + rendezvousIP=$(getRendezvousIP) + # Simulate user actions as done on the webUI and start cluster installation + RENDEZVOUS_IP=$rendezvousIP OCP_DIR=$OCP_DIR go run agent/isobuilder/ui_driven_cluster_installation.go + # To Do: Wait for the cluster install to finish + exit 0 ;; esac -if [[ "${AGENT_E2E_TEST_BOOT_MODE}" == "ISO_NO_REGISTRY" ]]; then - # Current goal is to only verify if the nodes are booted fine, - # TUI sets the rendezvous IP correctly and UI is accessible. - # The next goal is to simulate adding the cluster details via UI - # and complete the cluster installation. - exit 0 -fi - if [ ! -z "${AGENT_TEST_CASES:-}" ]; then run_agent_test_cases fi diff --git a/agent/agent_post_install_validation.sh b/agent/agent_post_install_validation.sh index 817457c56..5e6a4ac51 100755 --- a/agent/agent_post_install_validation.sh +++ b/agent/agent_post_install_validation.sh @@ -7,6 +7,8 @@ source $SCRIPTDIR/common.sh # Temp code skip the execution flow as cluster is not really installed if [[ "${AGENT_E2E_TEST_BOOT_MODE}" == "ISO_NO_REGISTRY" ]]; then + # To Do: Using export KUBECONFIG="${SCRIPTDIR}/ocp/$CLUSTER_NAME/auth/kubeconfig" which is set after downloading the kubeconfig, + # wait untill we get nodes and cluster is completely installed exit 0 fi diff --git a/agent/isobuilder/ui_driven_cluster_installation.go b/agent/isobuilder/ui_driven_cluster_installation.go new file mode 100644 index 000000000..114d635fe --- /dev/null +++ b/agent/isobuilder/ui_driven_cluster_installation.go @@ -0,0 +1,284 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + "log" + + resty "github.com/go-resty/resty/v2" + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/launcher" + "github.com/go-rod/rod/lib/proto" + "github.com/go-rod/rod/lib/utils" +) + +func main() { + fmt.Println("Launching headless browser...") + url := launcher.New().Headless(true).MustLaunch() + browser := rod.New().ControlURL(url).MustConnect() + defer browser.MustClose() + + rendezvousIP := os.Getenv("RENDEZVOUS_IP") + page := browser.MustPage(fmt.Sprintf("http://%s:3001/", rendezvousIP)) + page.MustWaitLoad() + + cwd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + ocpDir := os.Getenv("OCP_DIR") + screenshotPath := filepath.Join(cwd, ocpDir) + + fmt.Println("==== Enter cluster details ====") + err = cluster_details(page, screenshotPath) + if err != nil { + log.Fatalf("failed to enter cluster details: %v", err) + } + next(page) + + fmt.Println("==== Select virtualization bundle ====") + err = virtualization_bundle(page, screenshotPath) + if err != nil { + log.Fatalf("failed to select virtualization bundle: %v", err) + } + + next(page) + + fmt.Println("==== Await host discovery ====") + err = host_discovery(page, screenshotPath) + if err != nil { + log.Fatalf("failed awaiting host discovery: %v", err) + } + + next(page) + + fmt.Println("==== Verify storage ====") + err = verify_storage(page, screenshotPath) + if err != nil { + log.Fatalf("failed awaiting host discovery: %v", err) + } + + next(page) + + fmt.Println("==== Enter networking details ====") + err = networking_details(page, screenshotPath) + if err != nil { + log.Fatalf("failed entering networking details: %v", err) + } + + next(page) + + fmt.Println("==== Download credentials ====") + err = download_credentials(page, screenshotPath, rendezvousIP) + if err != nil { + log.Fatalf("failed downloading credentials: %v", err) + } + next(page) + + fmt.Println("==== Review ====") + err =review(page, screenshotPath) + if err != nil { + log.Fatalf("failed review page: %v", err) + } + + fmt.Println("==== Installation progress ====") + err = installation_progress(page, screenshotPath) + if err != nil { + log.Fatalf("failed review page: %v", err) + } + fmt.Println("Cluster installation started successfully.") +} + +func cluster_details(page *rod.Page, screenshotPath string) error { + page.MustElement("#form-input-name-field").MustInput("abi-ove-isobuilder") + page.MustElement("#form-input-baseDnsDomain-field").MustInput("redhat.com") + + pullSecretPath := os.Getenv("PULL_SECRET_FILE") + secretBytes, err := os.ReadFile(pullSecretPath) + if err != nil { + return fmt.Errorf("failed to read pull secret file: %v", err) + } + pullSecret := strings.TrimSpace(string(secretBytes)) + page.MustElement("#form-input-pullSecret-field").MustInput(pullSecret) + + path := filepath.Join(screenshotPath, "01-cluster-details.png") + err = saveFullPageScreenshot(page, path) + if err != nil { + return err + } + return nil +} + +func virtualization_bundle(page *rod.Page, screenshotPath string) error { + page.MustElement(`#bundle-virtualization`).MustClick().MustWaitEnabled() + path := filepath.Join(screenshotPath, "02-operators.png") + err := saveFullPageScreenshot(page, path) + if err != nil { + return err + } + return nil +} + +func host_discovery(page *rod.Page, screenshotPath string) error { + page.MustElement(`button[name="next"]`).MustWaitEnabled() + path := filepath.Join(screenshotPath, "03-hostDiscovery.png") + err := saveFullPageScreenshot(page, path) + if err != nil { + return err + } + return nil +} + +func verify_storage(page *rod.Page, screenshotPath string) error { + path := filepath.Join(screenshotPath, "04-storage.png") + err := saveFullPageScreenshot(page, path) + if err != nil { + return err + } + return nil +} + +func networking_details(page *rod.Page, screenshotPath string) error { + // To Do: Do not hardcode the VIPS + page.MustElement("#form-input-apiVips-0-ip-field").MustInput("192.168.111.50") + page.MustElement("#form-input-ingressVips-0-ip-field").MustInput("192.168.111.51") + page.MustElement(`button[name="next"]`).MustWaitEnabled() + + path := filepath.Join(screenshotPath, "05-networking.png") + err := saveFullPageScreenshot(page, path) + if err != nil { + return err + } + return nil +} + +func download_credentials(page *rod.Page, screenshotPath, rendezvousIP string) error { + page.MustElement(`#credentials-download-agreement`).MustClick() + page.MustElement(`button[name="next"]`).MustWaitEnabled() + + path := filepath.Join(screenshotPath, "06-credentials.png") + err := saveFullPageScreenshot(page, path) + if err != nil { + return err + } + time.Sleep(30 * time.Second) + + client := resty.New() + apiURL := fmt.Sprintf("http://%s:3001/api/assisted-install/v2/clusters/", rendezvousIP) + clusterID, err := getClusterID(client, apiURL) + if err != nil { + return err + } + apiURL = apiURL+clusterID + + fmt.Println("Download credentials via api request") + + downloadCredentials(client, apiURL, "kubeadmin-password") + time.Sleep(15 * time.Second) + + downloadCredentials(client, apiURL, "kubeconfig") + time.Sleep(15 * time.Second) + return nil +} + +func review(page *rod.Page, screenshotPath string) error { + path := filepath.Join(screenshotPath, "07-review.png") + err := saveFullPageScreenshot(page, path) + if err != nil { + return err + } + page.MustElement(`button[name="install"]`).MustClick() + fmt.Println("install button clicked...") + return nil +} + +func installation_progress(page *rod.Page, screenshotPath string) error { + page.MustElementR(`h2[data-ouia-component-type="PF5/Text"]`, `Installation progress`) + path := filepath.Join(screenshotPath, "08-installation.png") + err := saveFullPageScreenshot(page, path) + if err != nil { + return err + } + return nil +} + +func saveFullPageScreenshot(page *rod.Page, path string) error { + result, err := page.Evaluate(rod.Eval(`() => { + return { + width: document.body.scrollWidth, + height: document.body.scrollHeight + } + }`)) + if err != nil { + return fmt.Errorf("failed to evaluate page size: %w", err) + } + + width := int(result.Value.Get("width").Int()) + height := int(result.Value.Get("height").Int()) + + err = page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{ + Width: int(width), + Height: int(height), + DeviceScaleFactor: 1, + Mobile: false, + }) + if err != nil { + return fmt.Errorf("failed to set viewport: %w", err) + } + + screenshot, err := page.Screenshot(false, nil) + if err != nil { + return fmt.Errorf("failed to take screenshot: %w", err) + } + + if err := utils.OutputFile(path, screenshot); err != nil { + return fmt.Errorf("failed to save screenshot: %w", err) + } + fmt.Println("Screenshot saved to", path, ", with type of image/png") + return nil +} + +func next(page *rod.Page){ + page.MustElement(`button[name="next"]`).MustWaitEnabled().MustClick() +} + +func getClusterID(client *resty.Client, apiURL string) (string, error) { + var clusters []struct { + ID string `json:"id"` + } + + resp, err := client.R(). + SetResult(&clusters). + Get(apiURL) + if err != nil { + return "", err + } + + if resp.IsError() { + return "", err + } + return clusters[0].ID, nil +} + +func downloadCredentials(client *resty.Client, apiURL, filename string){ + fmt.Println("Downloading ", filename) + + apiURL = apiURL+"/downloads/credentials?file_name="+filename + resp, err := client.R().Get(apiURL) + if err != nil { + fmt.Println("Request failed:", err) + return + } + + downloadedFile:="/home/test/dev-scripts/ocp/ostest/auth/"+filename + err = os.WriteFile(downloadedFile, resp.Body(), 0644) + if err != nil { + fmt.Println("Failed to save file %s:", downloadedFile, err) + return + } + fmt.Println("File", downloadedFile, "downloaded successfully") +} + diff --git a/go.mod b/go.mod index 6b5458ad3..11c780266 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,14 @@ require ( github.com/openshift/installer v1.4.17 ) -require github.com/pkg/errors v0.9.1 // indirect +require ( + github.com/go-resty/resty/v2 v2.16.5 // indirect + github.com/go-rod/rod v0.116.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/ysmood/fetchup v0.2.3 // indirect + github.com/ysmood/goob v0.4.0 // indirect + github.com/ysmood/got v0.40.0 // indirect + github.com/ysmood/gson v0.7.3 // indirect + github.com/ysmood/leakless v0.9.0 // indirect + golang.org/x/net v0.33.0 // indirect +) diff --git a/go.sum b/go.sum index 08adacb78..360a5cf02 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,23 @@ github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= +github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= github.com/openshift/installer v1.4.17 h1:63iijBBgYqQX/p2+Q74gPqnfBN5VNSWX5LxQKuLlj6g= github.com/openshift/installer v1.4.17/go.mod h1:CtlMEGKJDVMZl4qVBC/xMUXM24YnleT6bakI+KXFAhk= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= +github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= +github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= +github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= +github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= +github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= +github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=