diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 4fcaaf6..4a3c5a0 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -1,12 +1,21 @@ name: Release Please + +# `next` accumulates validated SDK changes in one versioned PR to `main`. +# Merging that PR creates the GitHub release; the package publishing workflow +# runs from the release event. on: push: branches: + - next - main permissions: contents: read +concurrency: + group: release-please + cancel-in-progress: false + jobs: release-please: if: github.repository == 'kernel/hypeman-go' @@ -26,7 +35,68 @@ jobs: permission-pull-requests: write permission-workflows: write - - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1 - id: release + - name: Set up Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '18.20.2' + + - name: Set up pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 with: - token: ${{ steps.release-token.outputs.token }} + version: '9.11.0' + run_install: false + + - name: Build pinned release tooling + id: tooling + env: + RELEASE_PLEASE_DIR: ${{ runner.temp }}/release-please + RELEASE_PLEASE_SHA: a116e1e520e0f87824acf46a2e79c91d41e819d7 + run: | + set -euo pipefail + rm -rf "$RELEASE_PLEASE_DIR" + git init "$RELEASE_PLEASE_DIR" + git -C "$RELEASE_PLEASE_DIR" remote add origin https://github.com/stainless-api/release-please.git + git -C "$RELEASE_PLEASE_DIR" fetch --depth=1 origin "$RELEASE_PLEASE_SHA" + git -C "$RELEASE_PLEASE_DIR" checkout --detach FETCH_HEAD + pnpm --dir "$RELEASE_PLEASE_DIR" install --frozen-lockfile + pnpm --dir "$RELEASE_PLEASE_DIR" build + echo "cli=$RELEASE_PLEASE_DIR/build/src/bin/release-please.js" >> "$GITHUB_OUTPUT" + + - name: Open or update the release PR + if: github.ref_name == 'next' + env: + GH_TOKEN: ${{ steps.release-token.outputs.token }} + RELEASE_PLEASE: ${{ steps.tooling.outputs.cli }} + run: | + set -euo pipefail + node "$RELEASE_PLEASE" release-pr \ + --repo-url "$GITHUB_REPOSITORY" \ + --token "$GH_TOKEN" \ + --target-branch main \ + --changes-branch next + + - name: Remove the legacy promotion PR + if: github.ref_name == 'next' + env: + GH_TOKEN: ${{ steps.release-token.outputs.token }} + run: | + set -euo pipefail + legacy=$(gh pr list --repo "$GITHUB_REPOSITORY" --head stainless/release \ + --state open --json number --jq '.[].number') + for pr in $legacy; do + gh pr close "$pr" --repo "$GITHUB_REPOSITORY" \ + --comment "Superseded by the versioned release PR from next to main." + done + gh api -X DELETE "repos/$GITHUB_REPOSITORY/git/refs/heads/stainless/release" >/dev/null 2>&1 || true + + - name: Create the GitHub release + if: github.ref_name == 'main' + env: + GH_TOKEN: ${{ steps.release-token.outputs.token }} + RELEASE_PLEASE: ${{ steps.tooling.outputs.cli }} + run: | + set -euo pipefail + node "$RELEASE_PLEASE" github-release \ + --repo-url "$GITHUB_REPOSITORY" \ + --token "$GH_TOKEN" \ + --target-branch main diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml index 0199324..fd9042d 100644 --- a/.github/workflows/stlc-promote.yml +++ b/.github/workflows/stlc-promote.yml @@ -1,8 +1,12 @@ -name: Promote SDKs +name: Promote SDK changes -# Manually fast-forwards production main to the reviewed staging main. The -# ancestor check refuses divergent histories; this workflow never force-pushes. +# Staging is the generator's integration history. Production `next` is the +# developer-facing queue for the next release. This workflow combines the +# latest released state with validated staging changes, then advances `next`. +# Release automation maintains the single versioned PR from `next` to `main`. on: + push: + branches: [main] workflow_dispatch: {} permissions: @@ -12,7 +16,9 @@ jobs: promote: if: github.repository == 'kernel/hypeman-go-staging' runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }} - environment: production + concurrency: + group: stlc-promote + cancel-in-progress: true steps: - name: Check out staging uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -29,33 +35,99 @@ jobs: owner: kernel repositories: hypeman-go permission-contents: write + permission-pull-requests: write permission-workflows: write - - name: Fetch production main + - name: Fetch production branches + id: production env: GH_TOKEN: ${{ steps.production-token.outputs.token }} PRODUCTION_REPO: kernel/hypeman-go run: | - git remote add production "https://x-access-token:${GH_TOKEN}@github.com/${PRODUCTION_REPO}.git" + set -euo pipefail + git remote add production \ + "https://x-access-token:${GH_TOKEN}@github.com/${PRODUCTION_REPO}.git" git fetch production main - - - name: Check whether production already has staging's content - id: diff - run: | - MERGED=$(git merge-tree --write-tree production/main origin/main) || MERGED=conflict - PRODUCTION_TREE=$(git rev-parse 'production/main^{tree}') - if [ "$MERGED" = "$PRODUCTION_TREE" ]; then - echo "Production already contains staging's content. Nothing to promote." - echo "synced=true" >> "$GITHUB_OUTPUT" + if git ls-remote --exit-code --heads production next >/dev/null 2>&1; then + git fetch production next + echo "has_next=true" >> "$GITHUB_OUTPUT" else - echo "synced=false" >> "$GITHUB_OUTPUT" + echo "has_next=false" >> "$GITHUB_OUTPUT" fi - - name: Promote staging to production - if: steps.diff.outputs.synced == 'false' + - name: Prepare the next release branch + env: + APP_SLUG: ${{ steps.production-token.outputs.app-slug }} + GH_TOKEN: ${{ steps.production-token.outputs.token }} + HAS_NEXT: ${{ steps.production.outputs.has_next }} + PRODUCTION_REPO: kernel/hypeman-go run: | - if ! git merge-base --is-ancestor production/main origin/main; then - echo "::error title=Promote blocked::production/main is not an ancestor of staging main. Back-sync production first." + set -euo pipefail + bot_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id) + git config user.name "${APP_SLUG}[bot]" + git config user.email "${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com" + + open_conflict_pr() { + source_ref=$1 + source_name=$2 + advance_next=$3 + conflict_branch=stlc/promotion-conflict + + git merge --abort + existing=$(gh pr list --repo "$PRODUCTION_REPO" --base next \ + --head "$conflict_branch" --state open --json url --jq '.[0].url // ""') + if [ -n "$existing" ]; then + echo "::error title=SDK promotion blocked::Resolve the existing recovery PR: $existing" + exit 1 + fi + + if [ "$advance_next" = "true" ]; then + git push production HEAD:refs/heads/next + fi + git push production "$source_ref:refs/heads/$conflict_branch" --force + + body=$(mktemp) + printf '%s\n' \ + '## SDK promotion conflict' \ + '' \ + "The automated promotion could not merge $source_name into the pending next release." \ + '' \ + 'Resolve the conflicts on this branch, validate the SDK, mark this PR ready, and merge it with a merge commit.' \ + '' \ + 'After merging, rerun the staging Promote SDK changes workflow to include any newer generated changes.' \ + > "$body" + recovery_url=$(gh pr create --repo "$PRODUCTION_REPO" --draft \ + --base next --head "$conflict_branch" \ + --title 'chore: resolve SDK promotion conflict' --body-file "$body") + echo "::error title=SDK promotion conflict::Resolve the recovery PR: $recovery_url" exit 1 + } + + if [ "$HAS_NEXT" = "true" ]; then + git checkout -B stlc/promote-next production/next + else + git checkout -B stlc/promote-next production/main + fi + + if ! git merge-base --is-ancestor production/main HEAD; then + if ! git merge --no-edit production/main; then + open_conflict_pr production/main 'production main' false + fi + fi + if ! git merge-base --is-ancestor origin/main HEAD; then + if ! git merge --no-edit origin/main; then + open_conflict_pr origin/main 'validated staging changes' true + fi + fi + + if [ "$HAS_NEXT" = "true" ]; then + git merge-base --is-ancestor production/next HEAD fi - git push production origin/main:refs/heads/main + + - name: Update the pending release + env: + GH_TOKEN: ${{ steps.production-token.outputs.token }} + run: | + set -euo pipefail + git push production HEAD:refs/heads/next + echo "Updated production next; the versioned release PR will be opened or refreshed." diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7f3f5c8..d2d60a3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.23.0" + ".": "0.24.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index b4620cc..1c9b72d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1 +1 @@ -configured_endpoints: 58 +configured_endpoints: 61 diff --git a/CHANGELOG.md b/CHANGELOG.md index d177453..6d8da84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [0.24.0](https://github.com/kernel/hypeman-go/compare/v0.23.0...v0.24.0) (2026-08-12) + + +### Features + +* Add QEMU microvm hypervisor backend ([f8a5d45](https://github.com/kernel/hypeman-go/commit/f8a5d4586ff9c812b8b5f0aa342bb0b7b47bd3dc)) +* Add request header authorization to ingress rules ([fb25cdc](https://github.com/kernel/hypeman-go/commit/fb25cdc43a87da159d80da7560d87e300754907a)) +* chore(stlc): seal custom-code tracking files ([f89533d](https://github.com/kernel/hypeman-go/commit/f89533d396385abb4ea110bb33266e006f44a46a)) + ## 0.23.0 (2026-08-06) Full Changelog: [v0.22.0...v0.23.0](https://github.com/kernel/hypeman-go/compare/v0.22.0...v0.23.0) diff --git a/README.md b/README.md index 77705c3..cfeb011 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Or to pin the version: ```sh -go get -u 'github.com/kernel/hypeman-go@v0.23.0' +go get -u 'github.com/kernel/hypeman-go@v0.24.0' ``` diff --git a/api.md b/api.md index dfe4a3d..0776586 100644 --- a/api.md +++ b/api.md @@ -236,3 +236,21 @@ Methods: - client.Builds.Cancel(ctx context.Context, id string) error - client.Builds.Events(ctx context.Context, id string, query hypeman.BuildEventsParams) (\*hypeman.BuildEvent, error) - client.Builds.Get(ctx context.Context, id string) (\*hypeman.Build, error) + +# Pushes + +Params Types: + +- hypeman.CreatePushRequestParam +- hypeman.PushCredentialsParam + +Response Types: + +- hypeman.Push +- hypeman.PushStatus + +Methods: + +- client.Pushes.New(ctx context.Context, body hypeman.PushNewParams) (\*hypeman.Push, error) +- client.Pushes.List(ctx context.Context) (\*[]hypeman.Push, error) +- client.Pushes.Get(ctx context.Context, id string) (\*hypeman.Push, error) diff --git a/client.go b/client.go index 59b3749..ce1422c 100644 --- a/client.go +++ b/client.go @@ -28,6 +28,7 @@ type Client struct { Resources ResourceService Builders BuilderService Builds BuildService + Pushes PushService } // DefaultClientOptions read from the environment (HYPEMAN_API_KEY, @@ -70,6 +71,7 @@ func NewClient(opts ...option.RequestOption) (r Client) { r.Resources = NewResourceService(opts...) r.Builders = NewBuilderService(opts...) r.Builds = NewBuildService(opts...) + r.Pushes = NewPushService(opts...) return } diff --git a/ingress.go b/ingress.go index 8d4393d..46ded56 100644 --- a/ingress.go +++ b/ingress.go @@ -174,17 +174,19 @@ type IngressRule struct { Target IngressTarget `json:"target" api:"required"` // Auto-create HTTP to HTTPS redirect for this hostname (only applies when tls is // enabled) - RedirectHTTP bool `json:"redirect_http"` + RedirectHTTP bool `json:"redirect_http"` + RequestHeaderAuth IngressRuleRequestHeaderAuth `json:"request_header_auth"` // Enable TLS termination (certificate auto-issued via ACME). Tls bool `json:"tls"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - Match respjson.Field - Target respjson.Field - RedirectHTTP respjson.Field - Tls respjson.Field - ExtraFields map[string]respjson.Field - raw string + Match respjson.Field + Target respjson.Field + RedirectHTTP respjson.Field + RequestHeaderAuth respjson.Field + Tls respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } @@ -203,6 +205,30 @@ func (r IngressRule) ToParam() IngressRuleParam { return param.Override[IngressRuleParam](json.RawMessage(r.RawJSON())) } +type IngressRuleRequestHeaderAuth struct { + // Dedicated request header that must match before proxying. Reserved + // authentication, cookie, host, framing, proxy, and hop-by-hop headers are not + // allowed. + Header string `json:"header" api:"required"` + // Exact header value required before proxying. This sensitive value is persisted + // and returned by the API like instance environment variables; clients should hide + // it by default. + Value string `json:"value" api:"required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Header respjson.Field + Value respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r IngressRuleRequestHeaderAuth) RawJSON() string { return r.JSON.raw } +func (r *IngressRuleRequestHeaderAuth) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + // The properties Match, Target are required. type IngressRuleParam struct { Match IngressMatchParam `json:"match,omitzero" api:"required"` @@ -211,7 +237,8 @@ type IngressRuleParam struct { // enabled) RedirectHTTP param.Opt[bool] `json:"redirect_http,omitzero"` // Enable TLS termination (certificate auto-issued via ACME). - Tls param.Opt[bool] `json:"tls,omitzero"` + Tls param.Opt[bool] `json:"tls,omitzero"` + RequestHeaderAuth IngressRuleRequestHeaderAuthParam `json:"request_header_auth,omitzero"` paramObj } @@ -223,6 +250,27 @@ func (r *IngressRuleParam) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// The properties Header, Value are required. +type IngressRuleRequestHeaderAuthParam struct { + // Dedicated request header that must match before proxying. Reserved + // authentication, cookie, host, framing, proxy, and hop-by-hop headers are not + // allowed. + Header string `json:"header" api:"required"` + // Exact header value required before proxying. This sensitive value is persisted + // and returned by the API like instance environment variables; clients should hide + // it by default. + Value string `json:"value" api:"required"` + paramObj +} + +func (r IngressRuleRequestHeaderAuthParam) MarshalJSON() (data []byte, err error) { + type shadow IngressRuleRequestHeaderAuthParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *IngressRuleRequestHeaderAuthParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type IngressTarget struct { // Target instance name, ID, or capture reference. // diff --git a/ingress_test.go b/ingress_test.go index 68693c9..5671c96 100644 --- a/ingress_test.go +++ b/ingress_test.go @@ -38,7 +38,11 @@ func TestIngressNewWithOptionalParams(t *testing.T) { Port: 8080, }, RedirectHTTP: hypeman.Bool(true), - Tls: hypeman.Bool(true), + RequestHeaderAuth: hypeman.IngressRuleRequestHeaderAuthParam{ + Header: "X-Ingress-Verification", + Value: "0123456789abcdef0123456789abcdef", + }, + Tls: hypeman.Bool(true), }}, Tags: map[string]string{ "team": "backend", diff --git a/instance.go b/instance.go index 6be329d..50e5b37 100644 --- a/instance.go +++ b/instance.go @@ -679,9 +679,9 @@ type Instance struct { HealthStatus InstanceHealthStatus `json:"health_status"` // Hotplug memory size (human-readable) HotplugSize string `json:"hotplug_size"` - // Hypervisor running this instance + // Hypervisor backend running this instance // - // Any of "cloud-hypervisor", "firecracker", "qemu", "vz". + // Any of "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", "vz". Hypervisor InstanceHypervisor `json:"hypervisor"` // Network configuration of the instance Network InstanceNetwork `json:"network"` @@ -804,13 +804,14 @@ func (r *InstanceGPU) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// Hypervisor running this instance +// Hypervisor backend running this instance type InstanceHypervisor string const ( InstanceHypervisorCloudHypervisor InstanceHypervisor = "cloud-hypervisor" InstanceHypervisorFirecracker InstanceHypervisor = "firecracker" InstanceHypervisorQemu InstanceHypervisor = "qemu" + InstanceHypervisorQemuMicrovm InstanceHypervisor = "qemu-microvm" InstanceHypervisorVz InstanceHypervisor = "vz" ) @@ -1454,9 +1455,12 @@ type InstanceNewParams struct { // Workload health check policy. Health is reported separately from instance // lifecycle state. HealthCheck HealthCheckParam `json:"health_check,omitzero"` - // Hypervisor to use for this instance. Defaults to server configuration. + // Hypervisor backend to use for this instance. qemu uses the architecture-native + // standard board; qemu-microvm uses QEMU's minimal Linux amd64 board and does not + // support PCI devices, hotplug memory, or more than eight virtio-mmio devices. + // Defaults to server configuration. // - // Any of "cloud-hypervisor", "firecracker", "qemu", "vz". + // Any of "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", "vz". Hypervisor InstanceNewParamsHypervisor `json:"hypervisor,omitzero"` // Network configuration for the instance Network InstanceNewParamsNetwork `json:"network,omitzero"` @@ -1567,13 +1571,17 @@ func (r *InstanceNewParamsGPU) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// Hypervisor to use for this instance. Defaults to server configuration. +// Hypervisor backend to use for this instance. qemu uses the architecture-native +// standard board; qemu-microvm uses QEMU's minimal Linux amd64 board and does not +// support PCI devices, hotplug memory, or more than eight virtio-mmio devices. +// Defaults to server configuration. type InstanceNewParamsHypervisor string const ( InstanceNewParamsHypervisorCloudHypervisor InstanceNewParamsHypervisor = "cloud-hypervisor" InstanceNewParamsHypervisorFirecracker InstanceNewParamsHypervisor = "firecracker" InstanceNewParamsHypervisorQemu InstanceNewParamsHypervisor = "qemu" + InstanceNewParamsHypervisorQemuMicrovm InstanceNewParamsHypervisor = "qemu-microvm" InstanceNewParamsHypervisorVz InstanceNewParamsHypervisor = "vz" ) diff --git a/instancesnapshot.go b/instancesnapshot.go index d0aed99..97e5089 100644 --- a/instancesnapshot.go +++ b/instancesnapshot.go @@ -92,7 +92,7 @@ type InstanceSnapshotRestoreParams struct { // Optional hypervisor override. Allowed only when restoring from a Stopped // snapshot. Standby snapshots must restore with their original hypervisor. // - // Any of "cloud-hypervisor", "firecracker", "qemu", "vz". + // Any of "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", "vz". TargetHypervisor InstanceSnapshotRestoreParamsTargetHypervisor `json:"target_hypervisor,omitzero"` // Optional final state after restore. Defaults by snapshot kind: // @@ -120,6 +120,7 @@ const ( InstanceSnapshotRestoreParamsTargetHypervisorCloudHypervisor InstanceSnapshotRestoreParamsTargetHypervisor = "cloud-hypervisor" InstanceSnapshotRestoreParamsTargetHypervisorFirecracker InstanceSnapshotRestoreParamsTargetHypervisor = "firecracker" InstanceSnapshotRestoreParamsTargetHypervisorQemu InstanceSnapshotRestoreParamsTargetHypervisor = "qemu" + InstanceSnapshotRestoreParamsTargetHypervisorQemuMicrovm InstanceSnapshotRestoreParamsTargetHypervisor = "qemu-microvm" InstanceSnapshotRestoreParamsTargetHypervisorVz InstanceSnapshotRestoreParamsTargetHypervisor = "vz" ) diff --git a/internal/version.go b/internal/version.go index 834f803..5f694dc 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.23.0" // x-release-please-version +const PackageVersion = "0.24.0" // x-release-please-version diff --git a/push.go b/push.go new file mode 100644 index 0000000..3c74de5 --- /dev/null +++ b/push.go @@ -0,0 +1,176 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package hypeman + +import ( + "context" + "errors" + "fmt" + "net/http" + "slices" + "time" + + "github.com/kernel/hypeman-go/internal/apijson" + shimjson "github.com/kernel/hypeman-go/internal/encoding/json" + "github.com/kernel/hypeman-go/internal/requestconfig" + "github.com/kernel/hypeman-go/option" + "github.com/kernel/hypeman-go/packages/param" + "github.com/kernel/hypeman-go/packages/respjson" +) + +// PushService contains methods and other services that help with interacting with +// the hypeman API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewPushService] method instead. +type PushService struct { + Options []option.RequestOption +} + +// NewPushService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewPushService(opts ...option.RequestOption) (r PushService) { + r = PushService{} + r.Options = opts + return +} + +// Creates a push job that exports a hypeman image from the local OCI cache to a +// remote registry (e.g. AWS ECR, Docker Hub). Only images in the ready state can +// be pushed. +func (r *PushService) New(ctx context.Context, body PushNewParams, opts ...option.RequestOption) (res *Push, err error) { + opts = slices.Concat(r.Options, opts) + path := "pushes" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return res, err +} + +// Lists outbound image push jobs, newest first. +func (r *PushService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Push, err error) { + opts = slices.Concat(r.Options, opts) + path := "pushes" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return res, err +} + +// Get push details +func (r *PushService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Push, err error) { + opts = slices.Concat(r.Options, opts) + if id == "" { + err = errors.New("missing required id parameter") + return nil, err + } + path := fmt.Sprintf("pushes/%s", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return res, err +} + +// The properties Image, Target are required. +type CreatePushRequestParam struct { + // Hypeman image name to push (tag or digest form) + Image string `json:"image" api:"required"` + // Full remote reference to push to + Target string `json:"target" api:"required"` + // Allow pushing to plain-HTTP registries + Insecure param.Opt[bool] `json:"insecure,omitzero"` + // Registry credentials borrowed for this push only. When omitted, the server's own + // registry credentials are used. + Credentials PushCredentialsParam `json:"credentials,omitzero"` + paramObj +} + +func (r CreatePushRequestParam) MarshalJSON() (data []byte, err error) { + type shadow CreatePushRequestParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *CreatePushRequestParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type Push struct { + // Push job identifier + ID string `json:"id" api:"required"` + CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"` + // Cached manifest digest being pushed + Digest string `json:"digest" api:"required"` + // Hypeman image name (normalized ref) + Image string `json:"image" api:"required"` + // Any of "queued", "pushing", "pushed", "failed". + Status PushStatus `json:"status" api:"required"` + // Remote reference the image is pushed to + Target string `json:"target" api:"required"` + // Total compressed layer bytes pushed + Bytes int64 `json:"bytes"` + CompletedAt time.Time `json:"completed_at" api:"nullable" format:"date-time"` + // Error message + Error string `json:"error" api:"nullable"` + // Number of layers pushed + Layers int64 `json:"layers"` + // Position in the push queue + QueuePosition int64 `json:"queue_position" api:"nullable"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + CreatedAt respjson.Field + Digest respjson.Field + Image respjson.Field + Status respjson.Field + Target respjson.Field + Bytes respjson.Field + CompletedAt respjson.Field + Error respjson.Field + Layers respjson.Field + QueuePosition respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r Push) RawJSON() string { return r.JSON.raw } +func (r *Push) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Registry credentials borrowed for this push only. When omitted, the server's own +// registry credentials are used. +type PushCredentialsParam struct { + // Registry password or access token + Password param.Opt[string] `json:"password,omitzero" format:"password"` + // Bearer token for an Authorization header + RegistryToken param.Opt[string] `json:"registry_token,omitzero" format:"password"` + // Registry username + Username param.Opt[string] `json:"username,omitzero"` + paramObj +} + +func (r PushCredentialsParam) MarshalJSON() (data []byte, err error) { + type shadow PushCredentialsParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *PushCredentialsParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type PushStatus string + +const ( + PushStatusQueued PushStatus = "queued" + PushStatusPushing PushStatus = "pushing" + PushStatusPushed PushStatus = "pushed" + PushStatusFailed PushStatus = "failed" +) + +type PushNewParams struct { + CreatePushRequest CreatePushRequestParam + paramObj +} + +func (r PushNewParams) MarshalJSON() (data []byte, err error) { + return shimjson.Marshal(r.CreatePushRequest) +} +func (r *PushNewParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} diff --git a/push_test.go b/push_test.go new file mode 100644 index 0000000..b5c7d37 --- /dev/null +++ b/push_test.go @@ -0,0 +1,94 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package hypeman_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/internal/testutil" + "github.com/kernel/hypeman-go/option" +) + +func TestPushNewWithOptionalParams(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Pushes.New(context.TODO(), hypeman.PushNewParams{ + CreatePushRequest: hypeman.CreatePushRequestParam{ + Image: "docker.io/library/alpine:latest", + Target: "123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:v1", + Credentials: hypeman.PushCredentialsParam{ + Password: hypeman.String("password"), + RegistryToken: hypeman.String("registry_token"), + Username: hypeman.String("username"), + }, + Insecure: hypeman.Bool(true), + }, + }) + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestPushList(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Pushes.List(context.TODO()) + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestPushGet(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Pushes.Get(context.TODO(), "id") + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/release-please-config.json b/release-please-config.json index 058b84a..bfddad6 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -10,6 +10,7 @@ "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": false, "pull-request-header": "Automated Release PR", + "pull-request-footer": "Merge this pull request with a merge commit. Merging creates the GitHub release and publishes the package.", "pull-request-title-pattern": "release: ${version}", "changelog-sections": [ { diff --git a/resource.go b/resource.go index 8177cbe..e8b34c2 100644 --- a/resource.go +++ b/resource.go @@ -148,7 +148,7 @@ const ( type MemoryReclaimAction struct { AppliedReclaimBytes int64 `json:"applied_reclaim_bytes" api:"required"` AssignedMemoryBytes int64 `json:"assigned_memory_bytes" api:"required"` - // Any of "cloud-hypervisor", "firecracker", "qemu", "vz". + // Any of "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", "vz". Hypervisor MemoryReclaimActionHypervisor `json:"hypervisor" api:"required"` InstanceID string `json:"instance_id" api:"required"` InstanceName string `json:"instance_name" api:"required"` @@ -190,6 +190,7 @@ const ( MemoryReclaimActionHypervisorCloudHypervisor MemoryReclaimActionHypervisor = "cloud-hypervisor" MemoryReclaimActionHypervisorFirecracker MemoryReclaimActionHypervisor = "firecracker" MemoryReclaimActionHypervisorQemu MemoryReclaimActionHypervisor = "qemu" + MemoryReclaimActionHypervisorQemuMicrovm MemoryReclaimActionHypervisor = "qemu-microvm" MemoryReclaimActionHypervisorVz MemoryReclaimActionHypervisor = "vz" ) diff --git a/snapshot.go b/snapshot.go index f615439..df58796 100644 --- a/snapshot.go +++ b/snapshot.go @@ -97,7 +97,7 @@ type Snapshot struct { SizeBytes int64 `json:"size_bytes" api:"required"` // Source instance hypervisor at snapshot creation time // - // Any of "cloud-hypervisor", "firecracker", "qemu", "vz". + // Any of "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", "vz". SourceHypervisor SnapshotSourceHypervisor `json:"source_hypervisor" api:"required"` // Source instance ID at snapshot creation time SourceInstanceID string `json:"source_instance_id" api:"required"` @@ -152,6 +152,7 @@ const ( SnapshotSourceHypervisorCloudHypervisor SnapshotSourceHypervisor = "cloud-hypervisor" SnapshotSourceHypervisorFirecracker SnapshotSourceHypervisor = "firecracker" SnapshotSourceHypervisorQemu SnapshotSourceHypervisor = "qemu" + SnapshotSourceHypervisorQemuMicrovm SnapshotSourceHypervisor = "qemu-microvm" SnapshotSourceHypervisorVz SnapshotSourceHypervisor = "vz" ) @@ -202,7 +203,7 @@ type SnapshotForkParams struct { // Optional hypervisor override. Allowed only when forking from a Stopped snapshot. // Standby snapshots must fork with their original hypervisor. // - // Any of "cloud-hypervisor", "firecracker", "qemu", "vz". + // Any of "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", "vz". TargetHypervisor SnapshotForkParamsTargetHypervisor `json:"target_hypervisor,omitzero"` // Optional final state for the forked instance. Defaults by snapshot kind: // @@ -230,6 +231,7 @@ const ( SnapshotForkParamsTargetHypervisorCloudHypervisor SnapshotForkParamsTargetHypervisor = "cloud-hypervisor" SnapshotForkParamsTargetHypervisorFirecracker SnapshotForkParamsTargetHypervisor = "firecracker" SnapshotForkParamsTargetHypervisorQemu SnapshotForkParamsTargetHypervisor = "qemu" + SnapshotForkParamsTargetHypervisorQemuMicrovm SnapshotForkParamsTargetHypervisor = "qemu-microvm" SnapshotForkParamsTargetHypervisorVz SnapshotForkParamsTargetHypervisor = "vz" )