-
Notifications
You must be signed in to change notification settings - Fork 516
MCO-2411: Add AWS marketplace AMI band check target #6365
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
djoshy
wants to merge
2
commits into
openshift:main
Choose a base branch
from
djoshy:add-aws-marketplace-periodic-target
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| # check-aws-marketplace-skew | ||
|
|
||
| A `make` target an engineer runs occasionally to check whether published AWS Marketplace RHCOS | ||
| AMIs have drifted out of MCO's boot-image skew band, so a stale or mismatched Marketplace image | ||
| can be caught before customers hit it. | ||
|
|
||
| For each known Marketplace product code (OCP/OKE/OPP × x86_64/arm64, plus their EMEA x86_64 | ||
| variants — ROSA Classic is excluded, as it's being sunset), it fetches every published AMI and | ||
| checks whether **at least one** falls within an acceptable version band: | ||
|
|
||
| - **Floor**: `RHCOSVersionBootImageSkewLimit`/`OCPVersionBootImageSkewLimit` | ||
| (`pkg/controller/common/constants.go`) as they stood ~4 months ago, reconstructed from | ||
| `openshift/machine-config-operator`'s GitHub history for `--branch` — not persisted state — so | ||
| Marketplace gets a grace period to catch up after MCO bumps its skew limit. | ||
| - **Ceiling**: the pinned RHCOS version for `--branch`, fetched live from `openshift/installer`'s | ||
| `data/data/coreos/coreos-rhel-10.json` on the matching branch. | ||
|
|
||
| The check is existential, not singular: Marketplace can keep multiple AMI versions live at once, so | ||
| the current/default one being out of band doesn't necessarily mean there's no compliant option. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```console | ||
| $ make check-aws-marketplace-skew | ||
| Skew-limit floor: RHCOS 9.2 (OCP 4.13.0) | ||
| Installer ceiling (x86_64): 10.2 | ||
| Installer ceiling (arm64): 10.2 | ||
|
|
||
| PRODUCT PRODUCT ID RESULT MATCHED AMI DETAIL | ||
| OCP x86_64 59ead7de-2540-4653-a8b0-fa7926d5c845 PASS ami-0123456789abcdef0 9.6.20260210-0 | ||
| OKE x86_64 963b36c3-de6f-48ed-b802-2b38b2a2cdeb PASS ami-0fedcba9876543210 9.6.20260210-0 | ||
| ... | ||
| ``` | ||
|
|
||
| Pass flags through with `ARGS`, e.g. `make check-aws-marketplace-skew ARGS="--json"`. | ||
|
|
||
| Flags: | ||
|
|
||
| - `--region` (default `us-east-1`): AWS region to query `DescribeImages` in. A single region is | ||
| sufficient — the version signal lives in the AMI `Name`/`Description` text, which is consistent | ||
| across every region a Marketplace listing replicates to. | ||
| - `--profile`: named AWS profile to use. Defaults to whatever the standard credential chain | ||
| resolves — if `aws` CLI commands already work for you, this tool will too. | ||
| - `--branch` (default `main`): the release branch to check, applied on both sides — the MCO | ||
| skew-limit floor is reconstructed from `openshift/machine-config-operator`'s history for this | ||
| branch, and the RHCOS ceiling is fetched from `openshift/installer`'s copy of the same branch | ||
| name. Both are fetched live from GitHub; no local checkout of either repo is needed, so this | ||
| works the same regardless of what `origin` points at locally (e.g. a personal fork that doesn't | ||
| mirror release branches). | ||
| - `--json`: emit a structured JSON report instead of a human-readable table. | ||
|
|
||
| Exit code is non-zero if any product code fails its band check. | ||
|
|
||
| ## Credentials | ||
|
|
||
| Requires the `aws` CLI to be installed and on `PATH` — this tool shells out to | ||
| `aws ec2 describe-images` rather than using the AWS Go SDK, so it has no AWS SDK dependency of its | ||
| own and just inherits whatever credentials already make `aws` CLI commands work for you | ||
| (environment variables, shared config/credentials file, SSO sessions). Use `--profile` to select a | ||
| named profile, same as `aws --profile`. | ||
|
|
||
| The skew-limit floor also makes one `api.github.com` call per run (listing commits touching | ||
| `pkg/controller/common/constants.go`), which is unauthenticated by default and subject to GitHub's | ||
| 60-requests/hour-per-IP limit — easy to hit on a shared NAT. Set `GITHUB_TOKEN` (the standard env | ||
| var honored by `gh`, GitHub Actions, etc.) to raise that to 5000/hour; no new flag needed. The | ||
| `raw.githubusercontent.com` fetches (installer ceiling, and the floor's file-at-commit lookup) | ||
| aren't subject to this same limit and don't need a token. | ||
|
|
||
| ## Known gaps | ||
|
|
||
| - There are no AWS credentials in this repo's own CI, so `DescribeImages`/`CheckProduct` aren't | ||
| exercised end-to-end by `go test` — those tests use fixtures instead. Verified manually against | ||
| real Marketplace AMIs. | ||
| - Similarly, calls against the *real* GitHub endpoints aren't exercised by `go test` — this | ||
| package's tests run as part of the whole repo's presubmit suite, and a real network dependency | ||
| there would trade one devex tool's coverage for flakiness across every PR. `githubCommitsForPath` | ||
| itself (pagination, stop-on-short-page, 404 handling) *is* unit-tested against a local | ||
| `httptest.Server`, as are the pure parsing/selection functions (`parseGitHubCommitsJSON`, | ||
| `selectHistoricalCommit`, `parseSkewLimitConstants`, `parseStreamCeiling`) — only the thin | ||
| HTTP-call wrappers (`fetchGitHubCommitsPage`'s live request, `fetchRawGitHubFile`) are verified | ||
| manually against the real API instead. | ||
| - The skew-limit floor reconstruction fetches at most 500 commits (5 pages of 100 — GitHub's own | ||
| per-page cap) touching `pkg/controller/common/constants.go` on `--branch`, stopping early once a | ||
| page comes back short. Real history has 63 commits touching that file today, so this is | ||
| comfortable headroom, but if that count ever passes 500 for a branch, the "no commit predates the | ||
| grace window" fallback would pick the oldest of the *fetched* commits rather than the file's true | ||
| oldest revision. | ||
| - `openshift/installer`'s stream metadata filename is RHEL-major-version-specific | ||
| (`coreos-rhel-10.json`) — will need a manual update when RHEL 11 lands. Not building dynamic | ||
| discovery for this now. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "os" | ||
| "time" | ||
| ) | ||
|
|
||
| // githubRawURLTemplate points at raw.githubusercontent.com, which serves a file's content at any | ||
| // ref (branch, tag, or commit SHA) without needing a local checkout of the repo. | ||
| const githubRawURLTemplate = "https://raw.githubusercontent.com/%s/%s/%s/%s" | ||
|
|
||
| // githubAPIBaseURL is a var (not a const) so tests can point it at an httptest.Server instead of | ||
| // the real GitHub API. | ||
| var githubAPIBaseURL = "https://api.github.com" | ||
|
|
||
| // githubCommitsAPITemplate lists commits touching a path, most recent first. | ||
| const githubCommitsAPITemplate = "%s/repos/%s/%s/commits?sha=%s&path=%s&per_page=%d&page=%d" | ||
|
|
||
| const ( | ||
| // githubCommitsPerPage is GitHub's own hard cap — the API rejects/clamps anything higher. | ||
| githubCommitsPerPage = 100 | ||
| // githubCommitsMaxPages bounds the total history considered to githubCommitsPerPage * | ||
| // githubCommitsMaxPages = 500 commits. Fetching stops as soon as a page comes back short | ||
| // (the true end of history for that path), so real MCO history (63 commits touching | ||
| // pkg/controller/common/constants.go today) still costs a single request — this cap only | ||
| // matters for branches/files with much deeper history than that. | ||
| githubCommitsMaxPages = 5 | ||
| ) | ||
|
|
||
| // RefNotFoundError indicates a GitHub API request returned HTTP 404. Path is set only when the | ||
| // request was for a specific file (raw.githubusercontent.com), where a 404 can mean either the | ||
| // ref or the path doesn't exist and the response body gives no way to tell which; for the commits | ||
| // API, a 404 unambiguously means the ref doesn't exist, so Path is left empty there. | ||
| type RefNotFoundError struct { | ||
| Owner, Repo, Ref, Path, URL string | ||
| } | ||
|
|
||
| func (e *RefNotFoundError) Error() string { | ||
| if e.Path != "" { | ||
| return fmt.Sprintf("%s/%s: ref %q or path %q not found (%s returned 404)", e.Owner, e.Repo, e.Ref, e.Path, e.URL) | ||
| } | ||
| return fmt.Sprintf("%s/%s has no branch %q (%s returned 404)", e.Owner, e.Repo, e.Ref, e.URL) | ||
| } | ||
|
|
||
| // fetchRawGitHubFile fetches path's content at ref from owner/repo over HTTPS, with no local | ||
| // checkout of that repo required. | ||
| func fetchRawGitHubFile(ctx context.Context, owner, repo, ref, path string) ([]byte, error) { | ||
| url := fmt.Sprintf(githubRawURLTemplate, owner, repo, ref, path) | ||
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| resp, err := http.DefaultClient.Do(req) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to fetch %s: %w", url, err) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode == http.StatusNotFound { | ||
| return nil, &RefNotFoundError{Owner: owner, Repo: repo, Ref: ref, Path: path, URL: url} | ||
| } | ||
| if resp.StatusCode != http.StatusOK { | ||
| body, _ := io.ReadAll(resp.Body) | ||
| return nil, fmt.Errorf("fetching %s returned HTTP %d: %s", url, resp.StatusCode, string(body)) | ||
| } | ||
|
|
||
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to read response body from %s: %w", url, err) | ||
| } | ||
| return body, nil | ||
| } | ||
|
|
||
| // ghCommit is the subset of a GitHub API commit resource this tool needs. | ||
| type ghCommit struct { | ||
| SHA string | ||
| Date time.Time | ||
| } | ||
|
|
||
| // githubCommitsForPath lists commits touching path on branch in owner/repo, most recent first, via | ||
| // the GitHub REST API rather than a local git checkout. Paginates up to githubCommitsMaxPages, | ||
| // stopping as soon as a page comes back short of githubCommitsPerPage (the true end of history). | ||
| func githubCommitsForPath(ctx context.Context, owner, repo, branch, path string) ([]ghCommit, error) { | ||
| var all []ghCommit | ||
| for page := 1; page <= githubCommitsMaxPages; page++ { | ||
| commits, err := fetchGitHubCommitsPage(ctx, owner, repo, branch, path, page) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| all = append(all, commits...) | ||
| if len(commits) < githubCommitsPerPage { | ||
| break | ||
| } | ||
| } | ||
| return all, nil | ||
| } | ||
|
|
||
| // fetchGitHubCommitsPage fetches a single page of githubCommitsForPath's results. | ||
| func fetchGitHubCommitsPage(ctx context.Context, owner, repo, branch, path string, page int) ([]ghCommit, error) { | ||
| url := fmt.Sprintf(githubCommitsAPITemplate, githubAPIBaseURL, owner, repo, branch, path, githubCommitsPerPage, page) | ||
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| req.Header.Set("Accept", "application/vnd.github+json") | ||
| // api.github.com (unlike raw.githubusercontent.com) enforces a 60/hour unauthenticated rate | ||
| // limit per IP, shared across everything on that IP — cheap to blow through on a shared NAT. | ||
| // GITHUB_TOKEN is the standard env var convention (gh CLI, GitHub Actions, etc.); honoring it | ||
| // here raises the limit to 5000/hour with zero new flags for the common case of already having | ||
| // one set. | ||
| if token := os.Getenv("GITHUB_TOKEN"); token != "" { | ||
| req.Header.Set("Authorization", "Bearer "+token) | ||
| } | ||
|
|
||
| resp, err := http.DefaultClient.Do(req) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to fetch %s: %w", url, err) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to read response body from %s: %w", url, err) | ||
| } | ||
|
|
||
| if resp.StatusCode == http.StatusNotFound { | ||
| return nil, &RefNotFoundError{Owner: owner, Repo: repo, Ref: branch, URL: url} | ||
| } | ||
| if resp.StatusCode != http.StatusOK { | ||
| return nil, fmt.Errorf("fetching %s returned HTTP %d: %s", url, resp.StatusCode, string(body)) | ||
| } | ||
|
|
||
| commits, err := parseGitHubCommitsJSON(body) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("branch %s: %w", branch, err) | ||
| } | ||
| return commits, nil | ||
| } | ||
|
|
||
| // parseGitHubCommitsJSON extracts SHA/committer-date pairs from a GitHub "list commits" API | ||
| // response, split out from githubCommitsForPath so it's testable against a fixture without a live | ||
| // network call. | ||
| func parseGitHubCommitsJSON(body []byte) ([]ghCommit, error) { | ||
| var raw []struct { | ||
| SHA string `json:"sha"` | ||
| Commit struct { | ||
| Committer struct { | ||
| Date time.Time `json:"date"` | ||
| } `json:"committer"` | ||
| } `json:"commit"` | ||
| } | ||
| if err := json.Unmarshal(body, &raw); err != nil { | ||
| return nil, fmt.Errorf("failed to parse commits list: %w", err) | ||
| } | ||
|
|
||
| commits := make([]ghCommit, 0, len(raw)) | ||
| for _, c := range raw { | ||
| commits = append(commits, ghCommit{SHA: c.SHA, Date: c.Commit.Committer.Date}) | ||
| } | ||
| return commits, nil | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.