Skip to content
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This document provides context for AI agents when working with the OAPE AI E2E F

## Purpose

This project provides AI-driven tools for end-to-end feature development in OpenShift operators. The workflow takes an Enhancement Proposal (EP) and/or design document (gist) and generates:
This project provides AI-driven tools for end-to-end feature development in OpenShift operators. The workflow takes an Enhancement Proposal (EP), design document (gist), and/or Jira ticket and generates:
1. API type definitions (Go structs)
2. Integration tests for the API types
3. Controller/reconciler implementation code
Expand All @@ -14,9 +14,9 @@ This project provides AI-driven tools for end-to-end feature development in Open
| Command | Purpose |
| ---------------------------------------------------------------- | -------------------------------------------------------------- |
| `/oape:init <git-url> <base-branch>` | Clone a Git repository and checkout the base branch |
| `/oape:api-generate <EP-URL> [--design-doc <GIST-URL>]` | Generate Go API types from EP and/or design doc |
| `/oape:api-generate <EP-URL> [--design-doc <GIST-URL>] [--jira <TICKET>]` | Generate Go API types from EP, design doc, and/or Jira ticket |
| `/oape:api-generate-tests <path>` | Generate integration test suites for API types |
| `/oape:api-implement <EP-URL> [--design-doc <GIST-URL>]` | Generate controller code from EP and/or design doc + API types |
| `/oape:api-implement <EP-URL> [--design-doc <GIST-URL>] [--jira <TICKET>]` | Generate controller code from EP, design doc, and/or Jira ticket + API types |
| `/oape:analyze-rfe <rfe-key>` | Analyze RFE and output EPIC, user stories, and outcomes |
| `/oape:e2e-generate <base-branch>` | Generate e2e test artifacts from git diff against base branch |
| `/oape:predict-regressions <base-branch>` | Predict API regressions and breaking changes from git diff |
Expand All @@ -31,9 +31,13 @@ These commands support flexible input sources:
| -------------------- | ---------------------------------------------------------------------------------- |
| EP only | `/oape:api-generate https://github.com/openshift/enhancements/pull/1234` |
| Design doc only | `/oape:api-generate --design-doc https://gist.github.com/user/abc123` |
| Jira ticket only | `/oape:api-generate --jira OCPBUGS-12345` |
| Jira ticket (URL) | `/oape:api-generate --jira https://issues.redhat.com/browse/OCPBUGS-12345` |
| EP + Design doc | `/oape:api-generate https://github.com/openshift/enhancements/pull/1234 --design-doc https://gist.github.com/user/abc123` |
| EP + Jira ticket | `/oape:api-generate https://github.com/openshift/enhancements/pull/1234 --jira OCPBUGS-12345` |
| All three sources | `/oape:api-generate https://github.com/openshift/enhancements/pull/1234 --design-doc https://gist.github.com/user/abc123 --jira OCPBUGS-12345` |

When both sources are provided, the design document takes precedence for implementation details while the EP provides high-level context.
When multiple sources are provided, precedence is: design document > Jira ticket > EP. The design document provides exact implementation details, the Jira ticket provides specific requirements and acceptance criteria, and the EP provides high-level context.

## Typical Workflow

Expand All @@ -47,6 +51,12 @@ When both sources are provided, the design document takes precedence for impleme
# 2b. Or generate API types with a detailed design document
/oape:api-generate https://github.com/openshift/enhancements/pull/XXXX --design-doc https://gist.github.com/user/my-design-doc

# 2c. Or generate API types from a Jira ticket
/oape:api-generate --jira OCPBUGS-12345

# 2d. Or combine Jira ticket with EP for richer context
/oape:api-generate https://github.com/openshift/enhancements/pull/XXXX --jira OCPBUGS-12345

# 3. Generate integration tests for the API types
/oape:api-generate-tests api/v1alpha1/

Expand All @@ -59,6 +69,9 @@ When both sources are provided, the design document takes precedence for impleme
# 5b. Or generate with detailed design document
/oape:api-implement https://github.com/openshift/enhancements/pull/XXXX --design-doc https://gist.github.com/user/my-design-doc

# 5c. Or generate from a Jira ticket
/oape:api-implement --jira OCPBUGS-12345

# 6. Build and verify
make generate && make manifests && make build && make test

Expand Down Expand Up @@ -131,6 +144,7 @@ Before running commands, ensure:
- **go** - Go toolchain installed
- **git** - Git installed
- **make** - Make installed
- **JIRA_PERSONAL_TOKEN** - Personal access token for Jira REST API (required when using `--jira` flag with api-generate or api-implement)

---

Expand Down
61 changes: 40 additions & 21 deletions go-server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ type App struct {
}

var epURLPattern = regexp.MustCompile(`^https://github\.com/openshift/enhancements/pull/\d+/?$`)
var gistURLPattern = regexp.MustCompile(`^https://gist\.github(usercontent)?\.com/`)

// CreateWorkflowRequest is the JSON body for POST /api/v1/workflows.
type CreateWorkflowRequest struct {
EPUrl string `json:"ep_url"`
BaseBranch string `json:"base_branch"`
RepoURL string `json:"repo_url"`
EPUrl string `json:"ep_url"`
BaseBranch string `json:"base_branch"`
RepoURL string `json:"repo_url"`
DesignDocURL string `json:"design_doc_url,omitempty"`
JiraTicket string `json:"jira_ticket,omitempty"`
}

// WorkflowSummary is a compact representation for workflow lists.
Expand All @@ -55,13 +58,15 @@ type RepoListResponse struct {

// WorkflowDetailResponse for GET /api/v1/workflows/{job_id}.
type WorkflowDetailResponse struct {
ID string `json:"id"`
Status string `json:"status"`
Message string `json:"message,omitempty"`
CreatedAt string `json:"createdAt"`
RepoURL string `json:"repoUrl"`
EPUrl string `json:"epUrl"`
BaseBranch string `json:"baseBranch"`
ID string `json:"id"`
Status string `json:"status"`
Message string `json:"message,omitempty"`
CreatedAt string `json:"createdAt"`
RepoURL string `json:"repoUrl"`
EPUrl string `json:"epUrl"`
BaseBranch string `json:"baseBranch"`
DesignDocURL string `json:"designDocUrl,omitempty"`
JiraTicket string `json:"jiraTicket,omitempty"`
}

// CreateWorkflowResponse for POST /api/v1/workflows.
Expand Down Expand Up @@ -145,16 +150,26 @@ func (a *App) HandleCreateWorkflow(w http.ResponseWriter, r *http.Request) {
return
}

if req.EPUrl == "" || req.RepoURL == "" || req.BaseBranch == "" {
writeError(w, http.StatusBadRequest, "ep_url, repo_url, and base_branch are required")
if req.RepoURL == "" || req.BaseBranch == "" {
writeError(w, http.StatusBadRequest, "repo_url and base_branch are required")
return
}

if !epURLPattern.MatchString(req.EPUrl) {
if req.EPUrl == "" && req.DesignDocURL == "" && req.JiraTicket == "" {
writeError(w, http.StatusBadRequest, "at least one input source is required: ep_url, design_doc_url, or jira_ticket")
return
}

if req.EPUrl != "" && !epURLPattern.MatchString(req.EPUrl) {
writeError(w, http.StatusBadRequest, "ep_url must be a valid OpenShift enhancement PR URL")
return
}

if req.DesignDocURL != "" && !gistURLPattern.MatchString(req.DesignDocURL) {
writeError(w, http.StatusBadRequest, "design_doc_url must be a valid GitHub Gist URL")
return
}

jobID, err := generateJobID()
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to generate job ID")
Expand All @@ -172,6 +187,8 @@ func (a *App) HandleCreateWorkflow(w http.ResponseWriter, r *http.Request) {
EPUrl: req.EPUrl,
RepoURL: req.RepoURL,
BaseBranch: req.BaseBranch,
DesignDocURL: req.DesignDocURL,
JiraTicket: req.JiraTicket,
WorkerImage: a.cfg.WorkerImage,
EnvConfigMap: a.cfg.WorkerEnvConfigMap,
GCloudSecret: a.cfg.GCloudSecretName,
Expand All @@ -188,7 +205,7 @@ func (a *App) HandleCreateWorkflow(w http.ResponseWriter, r *http.Request) {
return
}

log.Printf("Created workflow job %s for ep=%s repo=%s base_branch=%s", jobID, req.EPUrl, req.RepoURL, req.BaseBranch)
log.Printf("Created workflow job %s for ep=%s design_doc=%s jira=%s repo=%s base_branch=%s", jobID, req.EPUrl, req.DesignDocURL, req.JiraTicket, req.RepoURL, req.BaseBranch)
writeJSON(w, http.StatusCreated, CreateWorkflowResponse{
ID: jobID,
Status: "pending",
Expand Down Expand Up @@ -232,13 +249,15 @@ func (a *App) HandleGetWorkflow(w http.ResponseWriter, r *http.Request) {
}

writeJSON(w, http.StatusOK, WorkflowDetailResponse{
ID: info.ID,
Status: info.Status,
Message: info.Message,
CreatedAt: info.CreatedAt,
RepoURL: info.RepoURL,
EPUrl: info.EPUrl,
BaseBranch: info.BaseBranch,
ID: info.ID,
Status: info.Status,
Message: info.Message,
CreatedAt: info.CreatedAt,
RepoURL: info.RepoURL,
EPUrl: info.EPUrl,
BaseBranch: info.BaseBranch,
DesignDocURL: info.DesignDocURL,
JiraTicket: info.JiraTicket,
})
}

Expand Down
32 changes: 22 additions & 10 deletions go-server/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ type WorkflowParams struct {
EPUrl string
RepoURL string
BaseBranch string
DesignDocURL string
JiraTicket string
WorkerImage string
EnvConfigMap string
GCloudSecret string
Expand Down Expand Up @@ -91,9 +93,11 @@ func (c *K8sClient) CreateWorkflowJob(ctx context.Context, jobID string, params
"job-id": jobID,
},
Annotations: map[string]string{
"app-platform-shift.openshift.github.io/repo-url": params.RepoURL,
"app-platform-shift.openshift.github.io/ep-url": params.EPUrl,
"app-platform-shift.openshift.github.io/base-branch": params.BaseBranch,
"app-platform-shift.openshift.github.io/repo-url": params.RepoURL,
"app-platform-shift.openshift.github.io/ep-url": params.EPUrl,
"app-platform-shift.openshift.github.io/base-branch": params.BaseBranch,
"app-platform-shift.openshift.github.io/design-doc-url": params.DesignDocURL,
"app-platform-shift.openshift.github.io/jira-ticket": params.JiraTicket,
},
},
Spec: batchv1.JobSpec{
Expand All @@ -117,6 +121,8 @@ func (c *K8sClient) CreateWorkflowJob(ctx context.Context, jobID string, params
{Name: "EP_URL", Value: params.EPUrl},
{Name: "REPO_URL", Value: params.RepoURL},
{Name: "BASE_BRANCH", Value: params.BaseBranch},
{Name: "DESIGN_DOC_URL", Value: params.DesignDocURL},
{Name: "JIRA_TICKET", Value: params.JiraTicket},
{Name: "PYTHONUNBUFFERED", Value: "1"},
{Name: "GOOGLE_APPLICATION_CREDENTIALS", Value: "/secrets/gcloud/application_default_credentials.json"},
},
Expand Down Expand Up @@ -248,13 +254,15 @@ func (c *K8sClient) StreamPodLogs(ctx context.Context, podName string, follow bo

// JobInfo contains extended job information from K8s.
type JobInfo struct {
ID string
Status string
Message string
CreatedAt string
RepoURL string
EPUrl string
BaseBranch string
ID string
Status string
Message string
CreatedAt string
RepoURL string
EPUrl string
BaseBranch string
DesignDocURL string
JiraTicket string
}

// ListJobs returns all workflow jobs with app=shift-worker label.
Expand Down Expand Up @@ -303,6 +311,8 @@ func (c *K8sClient) ListJobs(ctx context.Context) ([]JobInfo, error) {
info.RepoURL = job.Annotations["app-platform-shift.openshift.github.io/repo-url"]
info.EPUrl = job.Annotations["app-platform-shift.openshift.github.io/ep-url"]
info.BaseBranch = job.Annotations["app-platform-shift.openshift.github.io/base-branch"]
info.DesignDocURL = job.Annotations["app-platform-shift.openshift.github.io/design-doc-url"]
info.JiraTicket = job.Annotations["app-platform-shift.openshift.github.io/jira-ticket"]
}

result = append(result, info)
Expand Down Expand Up @@ -349,6 +359,8 @@ func (c *K8sClient) GetJobInfo(ctx context.Context, jobID string) (*JobInfo, err
info.RepoURL = job.Annotations["app-platform-shift.openshift.github.io/repo-url"]
info.EPUrl = job.Annotations["app-platform-shift.openshift.github.io/ep-url"]
info.BaseBranch = job.Annotations["app-platform-shift.openshift.github.io/base-branch"]
info.DesignDocURL = job.Annotations["app-platform-shift.openshift.github.io/design-doc-url"]
info.JiraTicket = job.Annotations["app-platform-shift.openshift.github.io/jira-ticket"]
}

return info, nil
Expand Down
34 changes: 29 additions & 5 deletions go-server/static/homepage.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
<body>
<div class="card">
<h1>OAPE Operator Feature Developer</h1>
<p class="sub">Generate complete operator implementation from an OpenShift Enhancement Proposal</p>
<p class="sub">Generate complete operator implementation from an Enhancement Proposal, Design Document, and/or Jira Ticket</p>

<div class="workflow-info">
<h3>Workflow Overview</h3>
Expand Down Expand Up @@ -76,12 +76,26 @@ <h3>Workflow Overview</h3>
</div>

<div class="field-group">
<label for="ep_url">Enhancement Proposal PR URL</label>
<label for="ep_url">Enhancement Proposal PR URL <span style="color:#888;font-weight:normal">(optional)</span></label>
<input type="text" id="ep_url" name="ep_url"
placeholder="https://github.com/openshift/enhancements/pull/1234" required>
placeholder="https://github.com/openshift/enhancements/pull/1234">
<div class="field-hint">The enhancement proposal that describes the feature</div>
</div>

<div class="field-group">
<label for="design_doc_url">Design Document URL <span style="color:#888;font-weight:normal">(optional)</span></label>
<input type="text" id="design_doc_url" name="design_doc_url"
placeholder="https://gist.github.com/user/gist_id">
<div class="field-hint">GitHub Gist with detailed API specifications</div>
</div>

<div class="field-group">
<label for="jira_ticket">Jira Ticket <span style="color:#888;font-weight:normal">(optional)</span></label>
<input type="text" id="jira_ticket" name="jira_ticket"
placeholder="OCPBUGS-12345 or https://issues.redhat.com/browse/OCPBUGS-12345">
<div class="field-hint">Jira ticket with requirements and acceptance criteria</div>
</div>

<button type="submit" id="submitBtn">Start Workflow</button>
</form>

Expand Down Expand Up @@ -137,10 +151,16 @@ <h3>Workflow Overview</h3>
form.addEventListener('submit', async (e) => {
e.preventDefault();
const epUrl = document.getElementById('ep_url').value.trim();
const designDocUrl = document.getElementById('design_doc_url').value.trim();
const jiraTicket = document.getElementById('jira_ticket').value.trim();
const repo = document.getElementById('repo').value;
const baseBranch = document.getElementById('base_branch').value.trim();

if (!epUrl || !repo || !baseBranch) return;
if (!repo || !baseBranch) return;
if (!epUrl && !designDocUrl && !jiraTicket) {
statusEl.innerHTML = '<span class="error">At least one input source is required (EP URL, Design Document, or Jira Ticket)</span>';
return;
}

btn.disabled = true;
logEl.style.display = 'none';
Expand All @@ -150,7 +170,11 @@ <h3>Workflow Overview</h3>
statusEl.innerHTML = '<span class="spinner"></span> Submitting workflow job\u2026';

try {
const body = JSON.stringify({ep_url: epUrl, repo_url: repo, base_branch: baseBranch});
const payload = {repo_url: repo, base_branch: baseBranch};
if (epUrl) payload.ep_url = epUrl;
if (designDocUrl) payload.design_doc_url = designDocUrl;
if (jiraTicket) payload.jira_ticket = jiraTicket;
const body = JSON.stringify(payload);
const res = await fetch('/api/v1/workflows', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
Expand Down
25 changes: 25 additions & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
@@ -1,19 +1,44 @@
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46 h1:lsxEuwrXEAokXB9qhlbKWPpo3KMLZQ5WB5WLQRW1uq0=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8=
github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo=
k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8=
Loading