Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions agent/01_agent_requirements.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,7 @@ if [[ "${AGENT_E2E_TEST_BOOT_MODE}" == "ISCSI" ]]; then
fi

if [[ "${AGENT_E2E_TEST_BOOT_MODE}" == "ISO_NO_REGISTRY" ]]; then
curl -O https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm
sudo dnf install -y ./google-chrome-stable_current_x86_64.rpm
sudo dnf -y install xorriso coreos-installer syslinux skopeo
fi
38 changes: 6 additions & 32 deletions agent/06_agent_create_cluster.sh
Original file line number Diff line number Diff line change
Expand Up @@ -639,41 +639,15 @@ 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
mkdir -p $OCP_DIR/auth
rendezvousIP=$(getRendezvousIP)
get_vips
# Simulate user actions as done on the webUI and start cluster installation
RENDEZVOUS_IP=$rendezvousIP OCP_DIR=$OCP_DIR INGRESS_VIP=$INGRESS_VIPS API_VIP=$API_VIPS go run -mod vendor agent/isobuilder/ui_driven_cluster_installation.go
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocking point, but wouldn't it have been simpler to pass them as a regular args?

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
Expand Down
7 changes: 5 additions & 2 deletions agent/agent_post_install_validation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"

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
exit 0
oc wait clusterversion version --for=condition=Available=True --timeout=60m
oc get csv -A
oc get packagemanifests -n openshift-marketplace
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these two commands for? It doesn't seem they are testing anything. I was expecting a more explicit check on the list of installed operators

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oc get csv -A is not useful seems like. oc get packagemanifests -n openshift-marketplace gives a list of operators , e.g.

oc get packagemanifests -n openshift-marketplace 
Member
NAME                                CATALOG   AGE
kubernetes-nmstate-operator                   3d20h
kubevirt-hyperconverged                       3d20h
node-healthcheck-operator                     3d20h
fence-agents-remediation                      3d20h
mtv-operator                                  3d20h
cluster-kube-descheduler-operator             3d20h
node-maintenance-operator                     3d20h

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these two commands for? It doesn't seem they are testing anything. I was expecting a more explicit check on the list of installed operators

Could you suggest what else we should be testing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fi

installed_control_plane_nodes=$(oc get nodes --selector=node-role.kubernetes.io/master | grep -v AGE | wc -l)
Expand All @@ -18,3 +19,5 @@ if (( $NUM_MASTERS != $installed_control_plane_nodes )); then
echo "Post install validation failed. Expected $NUM_MASTERS control plane nodes but found $installed_control_plane_nodes."
exit 1
fi

oc get clusterversion
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And also this one

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this to be a good way to see the cluster version
e.g. oc get clusterversion NAME VERSION AVAILABLE PROGRESSING SINCE STATUS version 4.19.7 True False 3d19h Error while reconciling 4.19.7: the cluster operator insights is not available

324 changes: 324 additions & 0 deletions agent/isobuilder/ui_driven_cluster_installation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
package main

import (
"fmt"
"log"
"os"
"path"
"path/filepath"

"errors"
"strings"
"time"

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"

"github.com/sirupsen/logrus"
)

var (
rendezvousIP = os.Getenv("RENDEZVOUS_IP")
ocpDir = os.Getenv("OCP_DIR")
baseURL = fmt.Sprintf("http://%s:3001", rendezvousIP)
clustersURL = fmt.Sprintf("%s%s", baseURL, path.Join("/api/assisted-install/v2/clusters"))
)

func main() {
logrus.Info("Launching headless browser...")
url := launcher.New().NoSandbox(true).Headless(true).MustLaunch()
browser := rod.New().ControlURL(url).MustConnect()

defer browser.MustClose()

page := browser.MustPage(baseURL)
page.MustWaitLoad()

cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
screenshotPath := filepath.Join(cwd, ocpDir)

logrus.Info("Enter cluster details")
err = clusterDetails(page, filepath.Join(screenshotPath, "01-cluster-details.png"))
if err != nil {
log.Fatalf("failed to enter cluster details: %v", err)
}

next(page)

logrus.Info("Select virtualization bundle")
err = virtualizationBundle(page, filepath.Join(screenshotPath, "02-operators.png"))
if err != nil {
log.Fatalf("failed to select virtualization bundle: %v", err)
}

next(page)

logrus.Info("Await host discovery")
err = hostDiscovery(page, filepath.Join(screenshotPath, "03-hostDiscovery.png"))
if err != nil {
log.Fatalf("failed awaiting host discovery: %v", err)
}

next(page)

logrus.Info("Verify storage")
err = verifyStorage(page, filepath.Join(screenshotPath, "04-storage.png"))
if err != nil {
log.Fatalf("failed awaiting host discovery: %v", err)
}

next(page)

logrus.Info("Enter networking details")
err = networkingDetails(page, filepath.Join(screenshotPath, "05-networking.png"))
if err != nil {
log.Fatalf("failed entering networking details: %v", err)
}

next(page)

logrus.Info("Download credentials")
client := resty.New()
err = downloadCredentials(page, client, filepath.Join(screenshotPath, "06-credentials.png"))
if err != nil {
log.Fatalf("failed downloading credentials: %v", err)
}

next(page)

logrus.Info("Review")
err = review(page, filepath.Join(screenshotPath, "07-review.png"))
if err != nil {
log.Fatalf("failed review page: %v", err)
}

logrus.Info("Start Cluster Installation")
err = startInstallation(page, client, filepath.Join(screenshotPath, "08-installation.png"))
if err != nil {
log.Fatalf("failed to start installation page: %v", err)
}
logrus.Info("Cluster installation started successfully.")

err = waitForClusterConsoleLink(page, filepath.Join(screenshotPath, "09-installation-progress.png"))
if err != nil {
log.Fatalf("%v", err)
}
}

func clusterDetails(page *rod.Page, path 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)

err = saveFullPageScreenshot(page, path)
if err != nil {
return err
}
return nil
}

func virtualizationBundle(page *rod.Page, path string) error {
page.MustElement(`#bundle-virtualization`).MustClick().MustWaitEnabled()
err := saveFullPageScreenshot(page, path)
if err != nil {
return err
}
return nil
}

func hostDiscovery(page *rod.Page, path string) error {
page.MustElement(`button[name="next"]`).MustWaitEnabled()
err := saveFullPageScreenshot(page, path)
if err != nil {
return err
}
return nil
}

func verifyStorage(page *rod.Page, path string) error {
err := saveFullPageScreenshot(page, path)
if err != nil {
return err
}
return nil
}

func networkingDetails(page *rod.Page, path string) error {
apiVip := os.Getenv("API_VIP")
ingressVip := os.Getenv("INGRESS_VIP")
page.MustElement("#form-input-apiVips-0-ip-field").MustInput(apiVip)
page.MustElement("#form-input-ingressVips-0-ip-field").MustInput(ingressVip)
page.MustElement(`button[name="next"]`).MustWaitEnabled()

err := saveFullPageScreenshot(page, path)
if err != nil {
return err
}
return nil
}

func downloadCredentials(page *rod.Page, client *resty.Client, path string) error {
page.MustElement(`#credentials-download-agreement`).MustClick()
page.MustElement(`button[name="next"]`).MustWaitEnabled()

err := saveFullPageScreenshot(page, path)
if err != nil {
return err
}
time.Sleep(30 * time.Second)

clusterID, err := getClusterID(client, clustersURL)
if err != nil {
return err
}

logrus.Info("Download credentials via api request")

fileURL := fmt.Sprintf("%s/%s/downloads/credentials?file_name=", clustersURL, clusterID)
saveCredentials(client, fileURL, "kubeadmin-password")
time.Sleep(15 * time.Second)

saveCredentials(client, fileURL, "kubeconfig")
time.Sleep(15 * time.Second)
return nil
}

func review(page *rod.Page, path string) error {
err := saveFullPageScreenshot(page, path)
if err != nil {
return err
}
page.MustElement(`button[name="install"]`).MustClick()
logrus.Info("Install button clicked")
return nil
}

func startInstallation(page *rod.Page, client *resty.Client, path string) error {
page.MustElementR(`h2[data-ouia-component-type="PF5/Text"]`, `Installation progress`)
err := saveFullPageScreenshot(page, path)
if err != nil {
return err
}

return nil
}

func waitForClusterConsoleLink(page *rod.Page, path string) error {
for {
failMsg, _ := page.Timeout(5 * time.Second).ElementR("#cluster-progress-status-value", `Failed on`)
if failMsg != nil {
if visible, _ := failMsg.Visible(); visible {
if err := saveFullPageScreenshot(page, path); err != nil {
return err
}
return errors.New("cluster installation failed")
}
}

consoleURL, _ := page.Timeout(5 * time.Second).ElementR("button.pf-v5-c-button", `https://console-openshift-console.apps.abi-ove-isobuilder.redhat.com`)
if consoleURL != nil {
if visible, _ := consoleURL.Visible(); visible {
logrus.Info("Console URL is available.")
break
}
}

logrus.Info("Cluster installation in progress. Waiting for console URL to be available.")
time.Sleep(5 * time.Minute)
}

if err := saveFullPageScreenshot(page, path); err != nil {
return err
}

return nil
}

func next(page *rod.Page) {
page.MustElement(`button[name="next"]`).MustWaitEnabled().MustClick()
}

// saveFullPageScreenshot captures a full-page screenshot and saves it to the given path.
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)
}
logrus.Info("Screenshot saved to", path, ", with type of image/png")
return nil
}

// getClusterID fetches the first cluster ID from the given URL using the provided client.
func getClusterID(client *resty.Client, url string) (string, error) {
var clusters []struct {
ID string `json:"id"`
}

_, err := client.R().SetResult(&clusters).Get(url)
if err != nil {
return "", err
}

return clusters[0].ID, nil
}

// saveCredentials downloads a file from the given URL and saves it under the auth directory.
func saveCredentials(client *resty.Client, url, filename string) {
logrus.Info("Downloading ", filename)

fileURL := fmt.Sprintf("%s%s", url, filename)
resp, err := client.R().Get(fileURL)
if err != nil {
logrus.Info("Request failed:", err)
return
}

downloadedFile := fmt.Sprintf("%s/auth/%s", ocpDir, filename)
err = os.WriteFile(downloadedFile, resp.Body(), 0644)
if err != nil {
logrus.Errorf("Failed to save file %s: %v", downloadedFile, err)
return
}
logrus.Info("File ", downloadedFile, " downloaded successfully")
}
Loading